summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorEskil Abrahamsen Blomfeldt <eskil.abrahamsen-blomfeldt@qt.io>2019-01-14 10:20:37 +0100
committerEskil Abrahamsen Blomfeldt <eskil.abrahamsen-blomfeldt@qt.io>2019-01-21 14:13:01 +0000
commit888fb0ca5d5e18fe5d8b88ed721544bf204ac158 (patch)
tree694ce06751637d2a15dbfe4d3a8260c0e6938769
parent7d089843c32296ba1d48713388c809996149138d (diff)
Initial commit for Qt Lottie!
This implements a QML API to display graphics and animations exported by the Bodymovin plugin in AfterEffects. Done-by: Kari Hautamäki <kari.hautamaki@qt.io> Change-Id: Idbcfc5c947e97c1a5a70947a262fe1be17de7504 Reviewed-by: Eirik Aavitsland <eirik.aavitsland@qt.io>
-rw-r--r--.qmake.conf4
-rw-r--r--LICENSE.GPL3674
-rw-r--r--LICENSE.GPL3-EXCEPT704
-rw-r--r--qtlottie.pro1
-rw-r--r--src/bodymovin/beziereasing.cpp69
-rw-r--r--src/bodymovin/beziereasing_p.h61
-rw-r--r--src/bodymovin/bmbase.cpp246
-rw-r--r--src/bodymovin/bmbase_p.h112
-rw-r--r--src/bodymovin/bmbasictransform.cpp155
-rw-r--r--src/bodymovin/bmbasictransform_p.h87
-rw-r--r--src/bodymovin/bmconstants_p.h103
-rw-r--r--src/bodymovin/bmellipse.cpp115
-rw-r--r--src/bodymovin/bmellipse_p.h86
-rw-r--r--src/bodymovin/bmfill.cpp91
-rw-r--r--src/bodymovin/bmfill_p.h75
-rw-r--r--src/bodymovin/bmfilleffect.cpp110
-rw-r--r--src/bodymovin/bmfilleffect_p.h77
-rw-r--r--src/bodymovin/bmfreeformshape.cpp337
-rw-r--r--src/bodymovin/bmfreeformshape_p.h107
-rw-r--r--src/bodymovin/bmgfill.cpp236
-rw-r--r--src/bodymovin/bmgfill_p.h91
-rw-r--r--src/bodymovin/bmglobal.h44
-rw-r--r--src/bodymovin/bmgroup.cpp145
-rw-r--r--src/bodymovin/bmgroup_p.h76
-rw-r--r--src/bodymovin/bmlayer.cpp279
-rw-r--r--src/bodymovin/bmlayer_p.h107
-rw-r--r--src/bodymovin/bmpathtrimmer.cpp105
-rw-r--r--src/bodymovin/bmpathtrimmer_p.h78
-rw-r--r--src/bodymovin/bmproperty_p.h395
-rw-r--r--src/bodymovin/bmrect.cpp137
-rw-r--r--src/bodymovin/bmrect_p.h86
-rw-r--r--src/bodymovin/bmrepeater.cpp97
-rw-r--r--src/bodymovin/bmrepeater_p.h79
-rw-r--r--src/bodymovin/bmrepeatertransform.cpp110
-rw-r--r--src/bodymovin/bmrepeatertransform_p.h79
-rw-r--r--src/bodymovin/bmround.cpp113
-rw-r--r--src/bodymovin/bmround_p.h84
-rw-r--r--src/bodymovin/bmshape.cpp205
-rw-r--r--src/bodymovin/bmshape_p.h100
-rw-r--r--src/bodymovin/bmshapelayer.cpp156
-rw-r--r--src/bodymovin/bmshapelayer_p.h77
-rw-r--r--src/bodymovin/bmshapetransform.cpp116
-rw-r--r--src/bodymovin/bmshapetransform_p.h83
-rw-r--r--src/bodymovin/bmspatialproperty_p.h129
-rw-r--r--src/bodymovin/bmstroke.cpp151
-rw-r--r--src/bodymovin/bmstroke_p.h81
-rw-r--r--src/bodymovin/bmtrimpath.cpp178
-rw-r--r--src/bodymovin/bmtrimpath_p.h87
-rw-r--r--src/bodymovin/bodymovin.pro79
-rw-r--r--src/bodymovin/lottierenderer.cpp55
-rw-r--r--src/bodymovin/lottierenderer_p.h107
-rw-r--r--src/bodymovin/trimpath.cpp226
-rw-r--r--src/bodymovin/trimpath_p.h93
-rw-r--r--src/imports/doc/lottieanimation.qdocconf14
-rw-r--r--src/imports/doc/lottieanimationdoc1
-rw-r--r--src/imports/imports.pro24
-rw-r--r--src/imports/lottie_plugin.cpp49
-rw-r--r--src/imports/lottie_plugin.h46
-rw-r--r--src/imports/lottieanimation.cpp662
-rw-r--r--src/imports/lottieanimation.h163
-rw-r--r--src/imports/qmldir2
-rw-r--r--src/imports/rasterrenderer/batchrenderer.cpp296
-rw-r--r--src/imports/rasterrenderer/batchrenderer.h112
-rw-r--r--src/imports/rasterrenderer/lottierasterrenderer.cpp379
-rw-r--r--src/imports/rasterrenderer/lottierasterrenderer.h88
-rw-r--r--src/src.pro5
-rw-r--r--src/unsupported_features.txt41
-rw-r--r--sync.profile6
-rw-r--r--tests/auto/auto.pro2
-rw-r--r--tests/auto/bodymovin/bodymovin.pro2
-rw-r--r--tests/auto/bodymovin/shape/ellipse/ellipse.pro5
-rw-r--r--tests/auto/bodymovin/shape/ellipse/ellipse_animated_100x80at00to200x40at50100.json1
-rw-r--r--tests/auto/bodymovin/shape/ellipse/ellipse_direction.json1
-rw-r--r--tests/auto/bodymovin/shape/ellipse/ellipse_hidden.json1
-rw-r--r--tests/auto/bodymovin/shape/ellipse/ellipse_static_100x80.json1
-rw-r--r--tests/auto/bodymovin/shape/ellipse/tst_bmellipse.cpp315
-rw-r--r--tests/auto/bodymovin/shape/fill/.gitignore73
-rw-r--r--tests/auto/bodymovin/shape/fill/fill.pro5
-rw-r--r--tests/auto/bodymovin/shape/fill/fill_animated_red100_green0.json1
-rw-r--r--tests/auto/bodymovin/shape/fill/fill_hidden.json1
-rw-r--r--tests/auto/bodymovin/shape/fill/fill_static_red_100.json1
-rw-r--r--tests/auto/bodymovin/shape/fill/tst_bmfill.cpp236
-rw-r--r--tests/auto/bodymovin/shape/path/freeform_curve_animated.json1
-rw-r--r--tests/auto/bodymovin/shape/path/freeform_curve_static.json1
-rw-r--r--tests/auto/bodymovin/shape/path/freeform_direction.json1
-rw-r--r--tests/auto/bodymovin/shape/path/freeform_hidden.json1
-rw-r--r--tests/auto/bodymovin/shape/path/freeform_roto_animated.json1
-rw-r--r--tests/auto/bodymovin/shape/path/freeform_roto_static.json1
-rw-r--r--tests/auto/bodymovin/shape/path/freeform_triangle_animated.json1
-rw-r--r--tests/auto/bodymovin/shape/path/freeform_triangle_static.json1
-rw-r--r--tests/auto/bodymovin/shape/path/path.pro5
-rw-r--r--tests/auto/bodymovin/shape/path/tst_bmpath.cpp1164
-rw-r--r--tests/auto/bodymovin/shape/rect/rect.pro5
-rw-r--r--tests/auto/bodymovin/shape/rect/rect_animated_30x30_1515_rad0_to_50x50_7575_rad25.json1
-rw-r--r--tests/auto/bodymovin/shape/rect/rect_direction.json1
-rw-r--r--tests/auto/bodymovin/shape/rect/rect_hidden.json1
-rw-r--r--tests/auto/bodymovin/shape/rect/rect_static_30x30_5050_rad0.json1
-rw-r--r--tests/auto/bodymovin/shape/rect/tst_bmrect.cpp349
-rw-r--r--tests/auto/bodymovin/shape/repeater/repeater.pro5
-rw-r--r--tests/auto/bodymovin/shape/repeater/repeater_animated.json1
-rw-r--r--tests/auto/bodymovin/shape/repeater/repeater_hidden.json1
-rw-r--r--tests/auto/bodymovin/shape/repeater/repeater_static.json1
-rw-r--r--tests/auto/bodymovin/shape/repeater/tst_bmrepeater.cpp231
-rw-r--r--tests/auto/bodymovin/shape/repeatertransform/repeater_transform_animated.json1
-rw-r--r--tests/auto/bodymovin/shape/repeatertransform/repeater_transform_static.json1
-rw-r--r--tests/auto/bodymovin/shape/repeatertransform/repeatertransform.pro6
-rw-r--r--tests/auto/bodymovin/shape/repeatertransform/tst_bmrepeatertransform.cpp461
-rw-r--r--tests/auto/bodymovin/shape/shape.pro13
-rw-r--r--tests/auto/bodymovin/shape/shapelayer/shape_active_60to120.json1
-rw-r--r--tests/auto/bodymovin/shape/shapelayer/shape_mask_alphaclip.json1
-rw-r--r--tests/auto/bodymovin/shape/shapelayer/shape_mask_alphainvclip.json1
-rw-r--r--tests/auto/bodymovin/shape/shapelayer/shape_mask_lumaclip.json1
-rw-r--r--tests/auto/bodymovin/shape/shapelayer/shape_mask_lumainvclip.json1
-rw-r--r--tests/auto/bodymovin/shape/shapelayer/shapelayer.pro5
-rw-r--r--tests/auto/bodymovin/shape/shapelayer/tst_bmshapelayer.cpp197
-rw-r--r--tests/auto/bodymovin/shape/shapetransform/shapetransform.pro5
-rw-r--r--tests/auto/bodymovin/shape/shapetransform/shapetransform_animated.json1
-rw-r--r--tests/auto/bodymovin/shape/shapetransform/shapetransform_static.json1
-rw-r--r--tests/auto/bodymovin/shape/shapetransform/tst_bmshapetransform.cpp508
-rw-r--r--tests/auto/bodymovin/shape/stroke/stroke.pro5
-rw-r--r--tests/auto/bodymovin/shape/stroke/stroke_animated_blue5_white1.json213
-rw-r--r--tests/auto/bodymovin/shape/stroke/stroke_hidden.json1
-rw-r--r--tests/auto/bodymovin/shape/stroke/stroke_static_blue_2.json1
-rw-r--r--tests/auto/bodymovin/shape/stroke/tst_bmstroke.cpp349
-rw-r--r--tests/auto/bodymovin/shape/transform/transform.pro6
-rw-r--r--tests/auto/bodymovin/shape/transform/transform_animated.json1
-rw-r--r--tests/auto/bodymovin/shape/transform/transform_static.json1
-rw-r--r--tests/auto/bodymovin/shape/transform/tst_bmbasictransform.cpp413
-rw-r--r--tests/auto/bodymovin/shape/trimpath/trimpath.pro5
-rw-r--r--tests/auto/bodymovin/shape/trimpath/trimpath_animated_2080_0_to_0060_3x30.json1
-rw-r--r--tests/auto/bodymovin/shape/trimpath/trimpath_hidden.json1
-rw-r--r--tests/auto/bodymovin/shape/trimpath/trimpath_static_20to80.json1
-rw-r--r--tests/auto/bodymovin/shape/trimpath/tst_bmtrimpath.cpp311
-rw-r--r--tests/auto/imports/imports.pro30
-rw-r--r--tests/auto/imports/main.cpp3
-rw-r--r--tests/auto/imports/qml.qrc6
-rw-r--r--tests/auto/imports/rec_pos_col_opa.json1
-rw-r--r--tests/auto/imports/tst_main.qml88
-rw-r--r--tests/manual/featcheck/.gitignore73
-rw-r--r--tests/manual/featcheck/featcheck.pro22
-rw-r--r--tests/manual/featcheck/howtofind.txt7
-rw-r--r--tests/manual/featcheck/main.cpp82
-rw-r--r--tests/manual/html/content.json1
-rw-r--r--tests/manual/html/index.html47
-rw-r--r--tests/manual/manual.pro2
-rw-r--r--tests/manual/testApp/.gitignore73
-rw-r--r--tests/manual/testApp/easing_rect_translate.json1
-rw-r--r--tests/manual/testApp/gradient_fill.json1
-rw-r--r--tests/manual/testApp/layer_effect_fill.json1
-rw-r--r--tests/manual/testApp/layer_effect_slider.json1
-rw-r--r--tests/manual/testApp/linked_layers.json1
-rw-r--r--tests/manual/testApp/main.cpp50
-rw-r--r--tests/manual/testApp/main.qml132
-rw-r--r--tests/manual/testApp/path_and_ellipse.json1
-rw-r--r--tests/manual/testApp/qml.qrc45
-rw-r--r--tests/manual/testApp/rect_anchor_moved_static2.json1
-rw-r--r--tests/manual/testApp/rect_anchor_std_anim.json1
-rw-r--r--tests/manual/testApp/rect_anim_easing.json2
-rw-r--r--tests/manual/testApp/rect_gfill_linear.json1
-rw-r--r--tests/manual/testApp/rect_gfill_radial.json1
-rw-r--r--tests/manual/testApp/rect_move_bezier.json1
-rw-r--r--tests/manual/testApp/rect_negative_scale_anchors.json1
-rw-r--r--tests/manual/testApp/rect_negative_scale_pos.json1
-rw-r--r--tests/manual/testApp/rect_negative_scale_pos_anchors_rot.json2
-rw-r--r--tests/manual/testApp/rect_rotate.json2
-rw-r--r--tests/manual/testApp/rect_rotate_layer.json1
-rw-r--r--tests/manual/testApp/rect_rotate_layer2.json1
-rw-r--r--tests/manual/testApp/rect_rotate_shape.json1
-rw-r--r--tests/manual/testApp/rect_scale.json1
-rw-r--r--tests/manual/testApp/rect_scale_anchors_on_layer.json1
-rw-r--r--tests/manual/testApp/rect_scale_anchors_on_rect.json1
-rw-r--r--tests/manual/testApp/rect_skew.json1
-rw-r--r--tests/manual/testApp/rect_skew_axis.json1
-rw-r--r--tests/manual/testApp/repeater1.json1
-rw-r--r--tests/manual/testApp/repeater_group1.json1
-rw-r--r--tests/manual/testApp/repeater_group2.json1
-rw-r--r--tests/manual/testApp/shape_bezier_simple.json1
-rw-r--r--tests/manual/testApp/shape_circle.json1
-rw-r--r--tests/manual/testApp/shape_complex.json1
-rw-r--r--tests/manual/testApp/shape_complex2.json1
-rw-r--r--tests/manual/testApp/shape_static.json1
-rw-r--r--tests/manual/testApp/stroke.json1
-rw-r--r--tests/manual/testApp/testApp.pro34
-rw-r--r--tests/manual/testApp/trim_path1.json1
-rw-r--r--tests/manual/testApp/trim_path2.json1
-rw-r--r--tests/manual/testApp/trim_path2_individual.json1
-rw-r--r--tests/manual/testApp/trim_path2_offset.json1
-rw-r--r--tests/manual/testApp/trim_path_multiple1.json1
-rw-r--r--tests/manual/testApp/trim_path_multiple2.json1
-rw-r--r--tests/manual/testApp/trim_path_multiple3.json1
-rw-r--r--tests/manual/testApp/trim_path_multiple4.json1
-rw-r--r--tests/manual/testApp/trim_path_simultaneous2.json1
-rw-r--r--tests/tests.pro4
193 files changed, 15043 insertions, 0 deletions
diff --git a/.qmake.conf b/.qmake.conf
new file mode 100644
index 0000000..7b49e2c
--- /dev/null
+++ b/.qmake.conf
@@ -0,0 +1,4 @@
+load(qt_build_config)
+CONFIG += warning_clean
+
+MODULE_VERSION = 5.13.0
diff --git a/LICENSE.GPL3 b/LICENSE.GPL3
new file mode 100644
index 0000000..94a9ed0
--- /dev/null
+++ b/LICENSE.GPL3
@@ -0,0 +1,674 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users. We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors. You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+ To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights. Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received. You must make sure that they, too, receive
+or can get the source code. And you must show them these terms so they
+know their rights.
+
+ Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+ For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software. For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+ Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so. This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software. The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable. Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products. If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+ Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary. To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ TERMS AND CONDITIONS
+
+ 0. Definitions.
+
+ "This License" refers to version 3 of the GNU General Public License.
+
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+ 1. Source Code.
+
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+ The Corresponding Source for a work in source code form is that
+same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Use with the GNU Affero General Public License.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+ <one line to give the program's name and a brief idea of what it does.>
+ Copyright (C) <year> <name of author>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+ <program> Copyright (C) <year> <name of author>
+ This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+ You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+<http://www.gnu.org/licenses/>.
+
+ The GNU General Public License does not permit incorporating your program
+into proprietary programs. If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License. But first, please read
+<http://www.gnu.org/philosophy/why-not-lgpl.html>.
diff --git a/LICENSE.GPL3-EXCEPT b/LICENSE.GPL3-EXCEPT
new file mode 100644
index 0000000..b1cb1be
--- /dev/null
+++ b/LICENSE.GPL3-EXCEPT
@@ -0,0 +1,704 @@
+This is the GNU General Public License version 3, annotated with The
+Qt Company GPL Exception 1.0:
+
+-------------------------------------------------------------------------
+
+The Qt Company GPL Exception 1.0
+
+Exception 1:
+
+As a special exception you may create a larger work which contains the
+output of this application and distribute that work under terms of your
+choice, so long as the work is not otherwise derived from or based on
+this application and so long as the work does not in itself generate
+output that contains the output from this application in its original
+or modified form.
+
+Exception 2:
+
+As a special exception, you have permission to combine this application
+with Plugins licensed under the terms of your choice, to produce an
+executable, and to copy and distribute the resulting executable under
+the terms of your choice. However, the executable must be accompanied
+by a prominent notice offering all users of the executable the entire
+source code to this application, excluding the source code of the
+independent modules, but including any changes you have made to this
+application, under the terms of this license.
+
+
+-------------------------------------------------------------------------
+
+ GNU GENERAL PUBLIC LICENSE
+ Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users. We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors. You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+ To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights. Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received. You must make sure that they, too, receive
+or can get the source code. And you must show them these terms so they
+know their rights.
+
+ Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+ For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software. For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+ Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so. This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software. The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable. Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products. If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+ Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary. To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ TERMS AND CONDITIONS
+
+ 0. Definitions.
+
+ "This License" refers to version 3 of the GNU General Public License.
+
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+ 1. Source Code.
+
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+ The Corresponding Source for a work in source code form is that
+same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Use with the GNU Affero General Public License.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+ <one line to give the program's name and a brief idea of what it does.>
+ Copyright (C) <year> <name of author>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+ <program> Copyright (C) <year> <name of author>
+ This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+ You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+<http://www.gnu.org/licenses/>.
+
+ The GNU General Public License does not permit incorporating your program
+into proprietary programs. If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License. But first, please read
+<http://www.gnu.org/philosophy/why-not-lgpl.html>.
diff --git a/qtlottie.pro b/qtlottie.pro
new file mode 100644
index 0000000..58c33f2
--- /dev/null
+++ b/qtlottie.pro
@@ -0,0 +1 @@
+load(qt_parts)
diff --git a/src/bodymovin/beziereasing.cpp b/src/bodymovin/beziereasing.cpp
new file mode 100644
index 0000000..5a76531
--- /dev/null
+++ b/src/bodymovin/beziereasing.cpp
@@ -0,0 +1,69 @@
+/****************************************************************************
+**
+** Copyright (C) 2018 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the lottie-qt module of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:GPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 or (at your option) any later version
+** approved by the KDE Free Qt Foundation. The licenses are as published by
+** the Free Software Foundation and appearing in the file LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#include <beziereasing_p.h>
+
+QT_BEGIN_NAMESPACE
+
+void BezierEasing::addCubicBezierSegment(const QPointF &c1, const QPointF &c2, const QPointF &endPoint)
+{
+ mBezier = QBezier::fromPoints(QPointF(0.0, 0.0), c1, c2, endPoint);
+}
+
+qreal BezierEasing::valueForProgress(qreal progress) const
+{
+ qreal res = mBezier.pointAt(tForX(progress)).y();
+ return qBound(0.0, res, 1.0);
+}
+
+qreal BezierEasing::tForX(qreal x) const
+{
+ if (x <= 0.0)
+ return 0.0;
+ else if (x >= 1.0)
+ return 1.0;
+
+ qreal t0 = 0.0;
+ qreal t1 = 1.0;
+
+ for (int i = 0; i < 10; i++) { // 10 iterations gives error smaller than 0.001
+ qreal t = qreal(0.5) * (t0 + t1);
+ qreal a, b, c, d;
+ QBezier::coefficients(t, a, b, c, d);
+ qreal xt = a * mBezier.x1 + b * mBezier.x2 + c * mBezier.x3 + d * mBezier.x4;
+ if (xt < x)
+ t0 = t;
+ else
+ t1 = t;
+ }
+
+ return t0;
+}
+
+QT_END_NAMESPACE
diff --git a/src/bodymovin/beziereasing_p.h b/src/bodymovin/beziereasing_p.h
new file mode 100644
index 0000000..4b3772e
--- /dev/null
+++ b/src/bodymovin/beziereasing_p.h
@@ -0,0 +1,61 @@
+/****************************************************************************
+**
+** Copyright (C) 2018 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the lottie-qt module of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:GPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 or (at your option) any later version
+** approved by the KDE Free Qt Foundation. The licenses are as published by
+** the Free Software Foundation and appearing in the file LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#ifndef BEZIEREASING_P_H
+#define BEZIEREASING_P_H
+
+//
+// W A R N I N G
+// -------------
+//
+// This file is not part of the Qt API. It exists purely as an
+// implementation detail. This header file may change from version to
+// version without notice, or even be removed.
+//
+// We mean it.
+//
+
+#include <private/qbezier_p.h>
+
+QT_BEGIN_NAMESPACE
+
+class BezierEasing
+{
+public:
+ void addCubicBezierSegment(const QPointF &c1, const QPointF &c2, const QPointF &endPoint);
+ qreal valueForProgress(qreal progress) const;
+
+private:
+ qreal tForX(qreal x) const;
+ QBezier mBezier;
+};
+
+QT_END_NAMESPACE
+
+#endif // BEZIEREASING_P_H
diff --git a/src/bodymovin/bmbase.cpp b/src/bodymovin/bmbase.cpp
new file mode 100644
index 0000000..859137b
--- /dev/null
+++ b/src/bodymovin/bmbase.cpp
@@ -0,0 +1,246 @@
+/****************************************************************************
+**
+** Copyright (C) 2018 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the lottie-qt module of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:GPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 or (at your option) any later version
+** approved by the KDE Free Qt Foundation. The licenses are as published by
+** the Free Software Foundation and appearing in the file LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#include "bmbase_p.h"
+
+#include <QLoggingCategory>
+#include <QRegularExpression>
+#include <QRegularExpressionMatch>
+
+QT_BEGIN_NAMESPACE
+
+Q_LOGGING_CATEGORY(lcLottieQtBodymovinParser, "qt.lottieqt.bodymovin.parser");
+Q_LOGGING_CATEGORY(lcLottieQtBodymovinUpdate, "qt.lottieqt.bodymovin.update");
+Q_LOGGING_CATEGORY(lcLottieQtBodymovinRender, "qt.lottieqt.bodymovin.render");
+
+BMBase::BMBase(const BMBase &other)
+{
+ m_definition = other.m_definition;
+ m_type = other.m_type;
+ m_hidden = other.m_hidden;
+ m_name = other.m_name;
+ m_autoOrient = other.m_autoOrient;
+ for (BMBase *child : qAsConst(other.m_children)) {
+ BMBase *clone = child->clone();
+ clone->setParent(this);
+ addChild(clone);
+ }
+}
+
+BMBase::~BMBase()
+{
+ qDeleteAll(m_children);
+}
+
+BMBase *BMBase::clone() const
+{
+ return new BMBase(*this);
+}
+
+QString BMBase::name() const
+{
+ return m_name;
+}
+
+void BMBase::setName(const QString &name)
+{
+ m_name = name;
+}
+
+bool BMBase::setProperty(BMLiteral::PropertyType propertyName, QVariant value)
+{
+ for (BMBase *child : qAsConst(m_children)) {
+ bool changed = child->setProperty(propertyName, value);
+ if (changed)
+ return true;
+ }
+ return false;
+}
+
+int BMBase::type() const
+{
+ return m_type;
+}
+
+void BMBase::setType(int type)
+{
+ m_type = type;
+}
+
+void BMBase::addChild(BMBase *child, bool priority)
+{
+ if (priority)
+ m_children.push_front(child);
+ else
+ m_children.push_back(child);
+}
+
+QList<BMBase *> &BMBase::children()
+{
+ return m_children;
+}
+
+BMBase *BMBase::findChild(const QString &childName)
+{
+ if (name() == childName)
+ return this;
+
+ BMBase *found = nullptr;
+ for (BMBase *child : qAsConst(m_children)) {
+ found = child->findChild(childName);
+ if (found)
+ break;
+ }
+ return found;
+}
+
+void BMBase::updateProperties(int frame)
+{
+ if (m_hidden)
+ return;
+
+ for (BMBase *child : qAsConst(m_children))
+ child->updateProperties(frame);
+}
+
+void BMBase::render(LottieRenderer &renderer) const
+{
+ if (m_hidden)
+ return;
+
+ renderer.saveState();
+ for (BMBase *child : qAsConst(m_children)) {
+ if (child->m_hidden)
+ continue;
+ child->render(renderer);
+ }
+ renderer.restoreState();
+}
+
+void BMBase::resolveTopRoot()
+{
+ if (!m_topRoot) {
+ BMBase *p = this;
+ while (p) {
+ m_topRoot = p;
+ p = p->parent();
+ }
+ }
+ Q_ASSERT(m_topRoot);
+}
+
+BMBase *BMBase::topRoot() const
+{
+ return m_topRoot;
+}
+
+void BMBase::parse(const QJsonObject &definition)
+{
+ qCDebug(lcLottieQtBodymovinParser) << "BMBase::parse()";
+
+ m_definition = definition;
+
+ m_hidden = definition.value(QLatin1String("hd")).toBool(false);
+ m_name = definition.value(QLatin1String("nm")).toString();
+ m_matchName = definition.value(QLatin1String("mn")).toString();
+ m_autoOrient = definition.value(QLatin1String("ao")).toBool();
+
+ if (m_autoOrient)
+ qCWarning(lcLottieQtBodymovinParser)
+ << "Element has auto-orientation set, but it is not supported";
+}
+
+const QJsonObject &BMBase::definition() const
+{
+ return m_definition;
+}
+
+bool BMBase::active(int frame) const
+{
+ Q_UNUSED(frame);
+ return !m_hidden;
+}
+
+bool BMBase::hidden() const
+{
+ return m_hidden;
+}
+
+BMBase *BMBase::parent() const
+{
+ return m_parent;
+}
+
+void BMBase::setParent(BMBase *parent)
+{
+ m_parent = parent;
+}
+
+
+const QJsonObject BMBase::resolveExpression(const QJsonObject &definition)
+{
+ QString expr = definition.value(QLatin1String("x")).toString();
+
+ // If there is no expression, return the original object definition
+ if (expr.isEmpty())
+ return definition;
+
+ // Find out layer handle
+ resolveTopRoot();
+
+ QRegularExpression re(QStringLiteral("effect\\(\\'(.*?)\\'\\)\\(\\'(.*?)\\'\\)"));
+ QRegularExpressionMatch match = re.match(expr);
+ if (!match.hasMatch())
+ return definition;
+
+ QString effect = match.captured(1);
+ QString elementName = match.captured(2);
+
+ QJsonObject retVal = definition;
+
+ if (BMBase *source = m_topRoot->findChild(effect)) {
+ if (source->children().length())
+ retVal = source->children().at(0)->definition().value(QLatin1String("v")).toObject();
+ else
+ retVal = source->definition().value(QLatin1String("v")).toObject();
+ if (source->children().length() > 1)
+ qCWarning(lcLottieQtBodymovinParser) << "Effect source points"
+ "to a group that has"
+ "many children. The"
+ "first is be picked";
+ }
+
+ // Let users of the json know that it is originated from expression,
+ // so they can adjust their behavior accordingly
+ retVal.insert(QLatin1String("fromExpression"), true);
+
+ return retVal;
+}
+
+QT_END_NAMESPACE
diff --git a/src/bodymovin/bmbase_p.h b/src/bodymovin/bmbase_p.h
new file mode 100644
index 0000000..efbd4c0
--- /dev/null
+++ b/src/bodymovin/bmbase_p.h
@@ -0,0 +1,112 @@
+/****************************************************************************
+**
+** Copyright (C) 2018 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the lottie-qt module of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:GPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 or (at your option) any later version
+** approved by the KDE Free Qt Foundation. The licenses are as published by
+** the Free Software Foundation and appearing in the file LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#ifndef BMBASE_P_H
+#define BMBASE_P_H
+
+//
+// W A R N I N G
+// -------------
+//
+// This file is not part of the Qt API. It exists purely as an
+// implementation detail. This header file may change from version to
+// version without notice, or even be removed.
+//
+// We mean it.
+//
+
+#include <QJsonObject>
+#include <QList>
+
+#include <QtBodymovin/bmglobal.h>
+#include <QtBodymovin/private/bmconstants_p.h>
+
+#include <QtBodymovin/private/lottierenderer_p.h>
+
+QT_BEGIN_NAMESPACE
+
+class BODYMOVIN_EXPORT BMBase
+{
+public:
+ BMBase() = default;
+ explicit BMBase(const BMBase &other);
+ virtual ~BMBase();
+
+ virtual BMBase *clone() const;
+
+ virtual bool setProperty(BMLiteral::PropertyType propertyType, QVariant value);
+
+ QString name() const;
+ void setName(const QString &name);
+
+ int type() const;
+ void setType(int type);
+ virtual void parse(const QJsonObject &definition);
+
+ const QJsonObject& definition() const;
+
+ virtual bool active(int frame) const;
+ bool hidden() const;
+
+ BMBase *parent() const;
+ void setParent(BMBase *parent);
+ void addChild(BMBase *child, bool priority = false);
+ QList<BMBase *>& children();
+ virtual BMBase *findChild(const QString &childName);
+
+ virtual void updateProperties(int frame);
+ virtual void render(LottieRenderer &renderer) const;
+
+protected:
+ void resolveTopRoot();
+ BMBase *topRoot() const;
+ const QJsonObject resolveExpression(const QJsonObject& definition);
+
+protected:
+ QJsonObject m_definition;
+ int m_type;
+ bool m_hidden = false;
+ QString m_name;
+ QString m_matchName;
+ bool m_autoOrient = false;
+ BMBase *m_parent = nullptr;
+ QList<BMBase *> m_children;
+
+ friend class BMRasterRenderer;
+ friend class BMRenderer;
+
+private:
+ // Handle to the topmost element on which this element resides
+ // Will be resolved when traversing effects
+ BMBase *m_topRoot = nullptr;
+};
+
+QT_END_NAMESPACE
+
+#endif // BMBASE_P_H
diff --git a/src/bodymovin/bmbasictransform.cpp b/src/bodymovin/bmbasictransform.cpp
new file mode 100644
index 0000000..1f30b0a
--- /dev/null
+++ b/src/bodymovin/bmbasictransform.cpp
@@ -0,0 +1,155 @@
+/****************************************************************************
+**
+** Copyright (C) 2018 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the lottie-qt module of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:GPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 or (at your option) any later version
+** approved by the KDE Free Qt Foundation. The licenses are as published by
+** the Free Software Foundation and appearing in the file LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#include "bmbasictransform_p.h"
+
+#include <QJsonObject>
+
+#include "bmconstants_p.h"
+
+QT_BEGIN_NAMESPACE
+
+BMBasicTransform::BMBasicTransform(const BMBasicTransform &other)
+ : BMShape(other)
+{
+ m_direction = other.m_direction;
+ m_anchorPoint = other.m_anchorPoint;
+ m_splitPosition = other.m_splitPosition;
+ m_position = other.m_position;
+ m_xPos = other.m_xPos;
+ m_yPos = other.m_yPos;
+ m_scale = other.m_scale;
+ m_rotation = other.m_rotation;
+ m_opacity = other.m_opacity;
+}
+
+BMBasicTransform::BMBasicTransform(const QJsonObject &definition, BMBase *parent)
+{
+ setParent(parent);
+ construct(definition);
+}
+
+BMBase *BMBasicTransform::clone() const
+{
+ return new BMBasicTransform(*this);
+}
+
+void BMBasicTransform::construct(const QJsonObject &definition)
+{
+ BMBase::parse(definition);
+
+ qCDebug(lcLottieQtBodymovinParser)
+ << "BMBasicTransform::construct():" << m_name;
+
+ QJsonObject anchors = definition.value(QLatin1String("a")).toObject();
+ anchors = resolveExpression(anchors);
+ m_anchorPoint.construct(anchors);
+
+ if (definition.value(QLatin1String("p")).toObject().contains(QLatin1String("s"))) {
+ QJsonObject posX = definition.value(QLatin1String("p")).toObject().value(QLatin1String("x")).toObject();
+ posX = resolveExpression(posX);
+ m_xPos.construct(posX);
+
+ QJsonObject posY = definition.value(QLatin1String("p")).toObject().value(QLatin1String("y")).toObject();
+ posY = resolveExpression(posY);
+ m_yPos.construct(posY);
+
+ m_splitPosition = true;
+ } else {
+ QJsonObject position = definition.value(QLatin1String("p")).toObject();
+ position = resolveExpression(position);
+ m_position.construct(position);
+ }
+
+ QJsonObject scale = definition.value(QLatin1String("s")).toObject();
+ scale = resolveExpression(scale);
+ m_scale.construct(scale);
+
+ QJsonObject rotation = definition.value(QLatin1String("r")).toObject();
+ rotation = resolveExpression(rotation);
+ m_rotation.construct(rotation);
+
+ // If this is the base class for BMRepeaterTransform,
+ // opacity is not present
+ if (definition.contains(QLatin1String("o"))) {
+ QJsonObject opacity = definition.value(QLatin1String("o")).toObject();
+ opacity = resolveExpression(opacity);
+ m_opacity.construct(opacity);
+ }
+}
+
+void BMBasicTransform::updateProperties(int frame)
+{
+ if (m_splitPosition) {
+ m_xPos.update(frame);
+ m_yPos.update(frame);
+ } else
+ m_position.update(frame);
+ m_anchorPoint.update(frame);
+ m_scale.update(frame);
+ m_rotation.update(frame);
+ m_opacity.update(frame);
+}
+
+void BMBasicTransform::render(LottieRenderer &renderer) const
+{
+ renderer.render(*this);
+}
+
+QPointF BMBasicTransform::anchorPoint() const
+{
+ return m_anchorPoint.value();
+}
+
+QPointF BMBasicTransform::position() const
+{
+ if (m_splitPosition)
+ return QPointF(m_xPos.value(), m_yPos.value());
+ else
+ return m_position.value();
+}
+
+QPointF BMBasicTransform::scale() const
+{
+ // Scale the value to 0..1 to be suitable for Qt
+ return m_scale.value() / 100.0;
+}
+
+qreal BMBasicTransform::rotation() const
+{
+ return m_rotation.value();
+}
+
+qreal BMBasicTransform::opacity() const
+{
+ // Scale the value to 0..1 to be suitable for Qt
+ return m_opacity.value() / 100.0;
+}
+
+QT_END_NAMESPACE
diff --git a/src/bodymovin/bmbasictransform_p.h b/src/bodymovin/bmbasictransform_p.h
new file mode 100644
index 0000000..a42e646
--- /dev/null
+++ b/src/bodymovin/bmbasictransform_p.h
@@ -0,0 +1,87 @@
+/****************************************************************************
+**
+** Copyright (C) 2018 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the lottie-qt module of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:GPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 or (at your option) any later version
+** approved by the KDE Free Qt Foundation. The licenses are as published by
+** the Free Software Foundation and appearing in the file LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#ifndef BMBASICTRANSFORM_P_H
+#define BMBASICTRANSFORM_P_H
+
+//
+// W A R N I N G
+// -------------
+//
+// This file is not part of the Qt API. It exists purely as an
+// implementation detail. This header file may change from version to
+// version without notice, or even be removed.
+//
+// We mean it.
+//
+
+#include <QPointF>
+
+#include <QtBodymovin/private/bmshape_p.h>
+#include <QtBodymovin/private/bmproperty_p.h>
+#include <QtBodymovin/private/bmspatialproperty_p.h>
+
+QT_BEGIN_NAMESPACE
+
+class QJsonObject;
+
+class BODYMOVIN_EXPORT BMBasicTransform : public BMShape
+{
+public:
+ BMBasicTransform() = default;
+ explicit BMBasicTransform(const BMBasicTransform &other);
+ BMBasicTransform(const QJsonObject &definition, BMBase *parent = nullptr);
+
+ BMBase *clone() const override;
+
+ void construct(const QJsonObject &definition);
+
+ void updateProperties(int frame) override;
+ void render(LottieRenderer &renderer) const override;
+
+ QPointF anchorPoint() const;
+ virtual QPointF position() const;
+ QPointF scale() const;
+ qreal rotation() const;
+ qreal opacity() const;
+
+protected:
+ BMSpatialProperty m_anchorPoint;
+ bool m_splitPosition = false;
+ BMSpatialProperty m_position;
+ BMProperty<qreal> m_xPos;
+ BMProperty<qreal> m_yPos;
+ BMProperty2D<QPointF> m_scale;
+ BMProperty<qreal> m_rotation;
+ BMProperty<qreal> m_opacity;
+};
+
+QT_END_NAMESPACE
+
+#endif // BMBASICTRANSFORM_P_H
diff --git a/src/bodymovin/bmconstants_p.h b/src/bodymovin/bmconstants_p.h
new file mode 100644
index 0000000..046e633
--- /dev/null
+++ b/src/bodymovin/bmconstants_p.h
@@ -0,0 +1,103 @@
+/****************************************************************************
+**
+** Copyright (C) 2018 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the lottie-qt module of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:GPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 or (at your option) any later version
+** approved by the KDE Free Qt Foundation. The licenses are as published by
+** the Free Software Foundation and appearing in the file LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#ifndef BMCONSTANTS_P_H
+#define BMCONSTANTS_P_H
+
+//
+// W A R N I N G
+// -------------
+//
+// This file is not part of the Qt API. It exists purely as an
+// implementation detail. This header file may change from version to
+// version without notice, or even be removed.
+//
+// We mean it.
+//
+
+#include <QObject>
+#include <QLoggingCategory>
+
+#include <QtBodymovin/bmglobal.h>
+
+#define BM_LAYER_PRECOMP_IX 0x10000
+#define BM_LAYER_SOLID_IX 0x10001
+#define BM_LAYER_IMAGE_IX 0x10002
+#define BM_LAYER_NULL_IX 0x10004
+#define BM_LAYER_SHAPE_IX 0x10008
+#define BM_LAYER_TEXT_IX 0x1000f
+
+#define BM_EFFECT_FILL 0x20000
+
+#define BM_SHAPE_ELLIPSE_STR "el"
+#define BM_SHAPE_FILL_STR "fl"
+#define BM_SHAPE_GFILL_STR "gf"
+#define BM_SHAPE_GSTROKE_STR "gs"
+#define BM_SHAPE_GROUP_STR "gr"
+#define BM_SHAPE_RECT_STR "rc"
+#define BM_SHAPE_ROUND_STR "rd"
+#define BM_SHAPE_SHAPE_STR "sh"
+#define BM_SHAPE_STAR_STR "sr"
+#define BM_SHAPE_STROKE_STR "st"
+#define BM_SHAPE_TRIM_STR "tm"
+#define BM_SHAPE_TRANSFORM_STR "tr"
+#define BM_SHAPE_REPEATER_STR "rp"
+
+QT_BEGIN_NAMESPACE
+
+Q_DECLARE_LOGGING_CATEGORY(lcLottieQtBodymovinParser);
+Q_DECLARE_LOGGING_CATEGORY(lcLottieQtBodymovinUpdate);
+Q_DECLARE_LOGGING_CATEGORY(lcLottieQtBodymovinRender);
+Q_DECLARE_LOGGING_CATEGORY(lcLottieQtBodymovinRenderThread);
+
+class BODYMOVIN_EXPORT BMLiteral : public QObject
+{
+ Q_OBJECT
+public:
+ enum ElementType {
+ Animation = 0,
+ LayerImage,
+ LayerNull,
+ LayerPrecomp,
+ LayerShape
+ };
+
+ enum PropertyType {
+ RectPosition,
+ RectSize,
+ RectRoundness
+ };
+ Q_ENUM(PropertyType)
+
+ explicit BMLiteral(QObject *parent = nullptr) : QObject(parent) {}
+};
+
+QT_END_NAMESPACE
+
+#endif // BMCONSTANTS_P_H
diff --git a/src/bodymovin/bmellipse.cpp b/src/bodymovin/bmellipse.cpp
new file mode 100644
index 0000000..f550f43
--- /dev/null
+++ b/src/bodymovin/bmellipse.cpp
@@ -0,0 +1,115 @@
+/****************************************************************************
+**
+** Copyright (C) 2018 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the lottie-qt module of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:GPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 or (at your option) any later version
+** approved by the KDE Free Qt Foundation. The licenses are as published by
+** the Free Software Foundation and appearing in the file LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#include "bmellipse_p.h"
+
+#include <QJsonObject>
+#include <QRectF>
+
+#include "bmtrimpath_p.h"
+
+QT_BEGIN_NAMESPACE
+
+BMEllipse::BMEllipse(const BMEllipse &other)
+ : BMShape(other)
+{
+ m_position = other.m_position;
+ m_size = other.m_size;
+}
+
+BMEllipse::BMEllipse(const QJsonObject &definition, BMBase *parent)
+{
+ setParent(parent);
+ construct(definition);
+}
+
+BMBase *BMEllipse::clone() const
+{
+ return new BMEllipse(*this);
+}
+
+
+void BMEllipse::construct(const QJsonObject &definition)
+{
+ BMBase::parse(definition);
+ if (m_hidden)
+ return;
+
+ qCDebug(lcLottieQtBodymovinParser) << "BMEllipse::construct():" << m_name;
+
+ QJsonObject position = definition.value(QLatin1String("p")).toObject();
+ position = resolveExpression(position);
+ m_position.construct(position);
+
+ QJsonObject size = definition.value(QLatin1String("s")).toObject();
+ size = resolveExpression(size);
+ m_size.construct(size);
+
+ m_direction = definition.value(QLatin1String("d")).toInt();
+}
+
+bool BMEllipse::acceptsTrim() const
+{
+ return true;
+}
+
+void BMEllipse::updateProperties(int frame)
+{
+ m_position.update(frame);
+ m_size.update(frame);
+
+ // AE uses center of a shape as it's position,
+ // in Qt a translation is needed
+ QPointF pos = QPointF(m_position.value().x() - m_size.value().width() / 2,
+ m_position.value().y() - m_size.value().height() / 2);
+
+ m_path = QPainterPath();
+ m_path.arcMoveTo(QRectF(pos, m_size.value()), 90);
+ m_path.arcTo(QRectF(pos, m_size.value()), 90, -360);
+
+ if (m_direction)
+ m_path = m_path.toReversed();
+}
+
+void BMEllipse::render(LottieRenderer &renderer) const
+{
+ renderer.render(*this);
+}
+
+QPointF BMEllipse::position() const
+{
+ return m_position.value();
+}
+
+QSizeF BMEllipse::size() const
+{
+ return m_size.value();
+}
+
+QT_END_NAMESPACE
diff --git a/src/bodymovin/bmellipse_p.h b/src/bodymovin/bmellipse_p.h
new file mode 100644
index 0000000..a3d8809
--- /dev/null
+++ b/src/bodymovin/bmellipse_p.h
@@ -0,0 +1,86 @@
+/****************************************************************************
+**
+** Copyright (C) 2018 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the lottie-qt module of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:GPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 or (at your option) any later version
+** approved by the KDE Free Qt Foundation. The licenses are as published by
+** the Free Software Foundation and appearing in the file LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#ifndef BMELLIPSE_P_H
+#define BMELLIPSE_P_H
+
+//
+// W A R N I N G
+// -------------
+//
+// This file is not part of the Qt API. It exists purely as an
+// implementation detail. This header file may change from version to
+// version without notice, or even be removed.
+//
+// We mean it.
+//
+
+#include <QRect>
+#include <QPointF>
+#include <QBrush>
+#include <QPen>
+#include <QPainterPath>
+
+#include <QtBodymovin/private/bmshape_p.h>
+#include <QtBodymovin/private/bmproperty_p.h>
+#include <QtBodymovin/private/bmspatialproperty_p.h>
+#include <QtBodymovin/private/bmfill_p.h>
+#include <QtBodymovin/private/bmstroke_p.h>
+
+QT_BEGIN_NAMESPACE
+
+class QJsonObject;
+
+class BODYMOVIN_EXPORT BMEllipse : public BMShape
+{
+public:
+ BMEllipse() = default;
+ explicit BMEllipse(const BMEllipse &other);
+ BMEllipse(const QJsonObject &definition, BMBase *parent = nullptr);
+
+ BMBase *clone() const override;
+
+ void construct(const QJsonObject &definition);
+
+ void updateProperties(int frame) override;
+ void render(LottieRenderer &renderer) const override;
+
+ bool acceptsTrim() const override;
+
+ QPointF position() const;
+ QSizeF size() const;
+
+protected:
+ BMSpatialProperty m_position;
+ BMProperty2D<QSizeF> m_size;
+};
+
+QT_END_NAMESPACE
+
+#endif // BMELLIPSE_P_H
diff --git a/src/bodymovin/bmfill.cpp b/src/bodymovin/bmfill.cpp
new file mode 100644
index 0000000..76012b3
--- /dev/null
+++ b/src/bodymovin/bmfill.cpp
@@ -0,0 +1,91 @@
+/****************************************************************************
+**
+** Copyright (C) 2018 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the lottie-qt module of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:GPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 or (at your option) any later version
+** approved by the KDE Free Qt Foundation. The licenses are as published by
+** the Free Software Foundation and appearing in the file LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#include "bmfill_p.h"
+
+QT_BEGIN_NAMESPACE
+
+BMFill::BMFill(const BMFill &other)
+ : BMShape(other)
+{
+ m_color = other.m_color;
+ m_opacity = other.m_opacity;
+}
+
+BMFill::BMFill(const QJsonObject &definition, BMBase *parent)
+{
+ setParent(parent);
+ BMBase::parse(definition);
+ if (m_hidden)
+ return;
+
+ qCDebug(lcLottieQtBodymovinParser) << "BMFill::construct():" << m_name;
+
+ QJsonObject color = definition.value(QLatin1String("c")).toObject();
+ m_color.construct(color);
+
+ QJsonObject opacity = definition.value(QLatin1String("o")).toObject();
+ opacity = resolveExpression(opacity);
+ m_opacity.construct(opacity);
+}
+
+BMBase *BMFill::clone() const
+{
+ return new BMFill(*this);
+}
+
+void BMFill::updateProperties(int frame)
+{
+ m_color.update(frame);
+ m_opacity.update(frame);
+}
+
+void BMFill::render(LottieRenderer &renderer) const
+{
+ renderer.render(*this);
+}
+
+QColor BMFill::color() const
+{
+ QVector4D cVec = m_color.value();
+ QColor color;
+ qreal r = static_cast<qreal>(cVec.x());
+ qreal g = static_cast<qreal>(cVec.y());
+ qreal b = static_cast<qreal>(cVec.z());
+ qreal a = static_cast<qreal>(cVec.w());
+ color.setRgbF(r, g, b, a);
+ return color;
+}
+
+qreal BMFill::opacity() const
+{
+ return m_opacity.value();
+}
+
+QT_END_NAMESPACE
diff --git a/src/bodymovin/bmfill_p.h b/src/bodymovin/bmfill_p.h
new file mode 100644
index 0000000..060a2f0
--- /dev/null
+++ b/src/bodymovin/bmfill_p.h
@@ -0,0 +1,75 @@
+/****************************************************************************
+**
+** Copyright (C) 2018 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the lottie-qt module of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:GPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 or (at your option) any later version
+** approved by the KDE Free Qt Foundation. The licenses are as published by
+** the Free Software Foundation and appearing in the file LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#ifndef BMFILL_P_H
+#define BMFILL_P_H
+
+//
+// W A R N I N G
+// -------------
+//
+// This file is not part of the Qt API. It exists purely as an
+// implementation detail. This header file may change from version to
+// version without notice, or even be removed.
+//
+// We mean it.
+//
+
+#include <QColor>
+#include <QVector4D>
+
+#include <QtBodymovin/private/bmgroup_p.h>
+#include <QtBodymovin/private/bmproperty_p.h>
+
+QT_BEGIN_NAMESPACE
+
+class BODYMOVIN_EXPORT BMFill : public BMShape
+{
+public:
+ BMFill() = default;
+ explicit BMFill(const BMFill &other);
+ BMFill(const QJsonObject &definition, BMBase *parent = nullptr);
+
+ BMBase *clone() const override;
+
+ void updateProperties(int frame) override;
+
+ void render(LottieRenderer &renderer) const override;
+
+ QColor color() const;
+ qreal opacity() const;
+
+protected:
+ BMProperty4D<QVector4D> m_color;
+ BMProperty<qreal> m_opacity;
+};
+
+QT_END_NAMESPACE
+
+#endif // BMFILL_P_H
diff --git a/src/bodymovin/bmfilleffect.cpp b/src/bodymovin/bmfilleffect.cpp
new file mode 100644
index 0000000..34e0e63
--- /dev/null
+++ b/src/bodymovin/bmfilleffect.cpp
@@ -0,0 +1,110 @@
+/****************************************************************************
+**
+** Copyright (C) 2018 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the lottie-qt module of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:GPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 or (at your option) any later version
+** approved by the KDE Free Qt Foundation. The licenses are as published by
+** the Free Software Foundation and appearing in the file LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#include "bmfilleffect_p.h"
+
+#include <QJsonObject>
+#include <QJsonValue>
+
+#include "bmglobal.h"
+
+QT_BEGIN_NAMESPACE
+
+BMFillEffect::BMFillEffect(const BMFillEffect &other)
+ : BMBase(other)
+{
+ m_color = other.m_color;
+ m_opacity = other.m_opacity;
+}
+
+BMBase *BMFillEffect::clone() const
+{
+ return new BMFillEffect(*this);
+}
+
+void BMFillEffect::construct(const QJsonObject &definition)
+{
+ m_type = BM_EFFECT_FILL;
+
+ if (!definition.value(QLatin1String("hd")).toBool(true))
+ return;
+
+ QJsonArray properties = definition.value(QLatin1String("ef")).toArray();
+
+ // TODO: Check are property positions really fixed in the effect?
+
+ m_color.construct(properties.at(2).toObject().value(QLatin1String("v")).toObject());
+ m_opacity.construct(properties.at(6).toObject().value(QLatin1String("v")).toObject());
+
+ if (!qFuzzyCompare(properties.at(0).toObject().value(QLatin1String("v")).toObject().value(QLatin1String("k")).toDouble(), 0.0))
+ qCWarning(lcLottieQtBodymovinParser)<< "BMFillEffect: Property 'Fill mask' not supported";
+
+ if (!qFuzzyCompare(properties.at(1).toObject().value(QLatin1String("v")).toObject().value(QLatin1String("k")).toDouble(), 0.0))
+ qCWarning(lcLottieQtBodymovinParser) << "BMFillEffect: Property 'All masks' not supported";
+
+ if (!qFuzzyCompare(properties.at(3).toObject().value(QLatin1String("v")).toObject().value(QLatin1String("k")).toDouble(), 0.0))
+ qCWarning(lcLottieQtBodymovinParser) << "BMFillEffect: Property 'Invert' not supported";
+
+ if (!qFuzzyCompare(properties.at(4).toObject().value(QLatin1String("v")).toObject().value(QLatin1String("k")).toDouble(), 0.0))
+ qCWarning(lcLottieQtBodymovinParser) << "BMFillEffect: Property 'Horizontal feather' not supported";
+
+ if (!qFuzzyCompare(properties.at(5).toObject().value(QLatin1String("v")).toObject().value(QLatin1String("k")).toDouble(), 0.0))
+ qCWarning(lcLottieQtBodymovinParser) << "BMFillEffect: Property 'Vertical feather' not supported";
+
+}
+
+void BMFillEffect::updateProperties(int frame)
+{
+ m_color.update(frame);
+ m_opacity.update(frame);
+}
+
+void BMFillEffect::render(LottieRenderer &renderer) const
+{
+ renderer.render(*this);
+}
+
+QColor BMFillEffect::color() const
+{
+ QVector4D cVec = m_color.value();
+ QColor color;
+ qreal r = static_cast<qreal>(cVec.x());
+ qreal g = static_cast<qreal>(cVec.y());
+ qreal b = static_cast<qreal>(cVec.z());
+ qreal a = static_cast<qreal>(cVec.w());
+ color.setRgbF(r, g, b, a);
+ return color;
+}
+
+qreal BMFillEffect::opacity() const
+{
+ return m_opacity.value();
+}
+
+QT_END_NAMESPACE
diff --git a/src/bodymovin/bmfilleffect_p.h b/src/bodymovin/bmfilleffect_p.h
new file mode 100644
index 0000000..43888bb
--- /dev/null
+++ b/src/bodymovin/bmfilleffect_p.h
@@ -0,0 +1,77 @@
+/****************************************************************************
+**
+** Copyright (C) 2018 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the lottie-qt module of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:GPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 or (at your option) any later version
+** approved by the KDE Free Qt Foundation. The licenses are as published by
+** the Free Software Foundation and appearing in the file LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#ifndef BMFILLEFFECT_P_H
+#define BMFILLEFFECT_P_H
+
+//
+// W A R N I N G
+// -------------
+//
+// This file is not part of the Qt API. It exists purely as an
+// implementation detail. This header file may change from version to
+// version without notice, or even be removed.
+//
+// We mean it.
+//
+
+#include <QColor>
+#include <QVector4D>
+
+#include <QtBodymovin/private/bmbase_p.h>
+#include <QtBodymovin/private/bmproperty_p.h>
+
+QT_BEGIN_NAMESPACE
+
+class QJsonObject;
+
+class BODYMOVIN_EXPORT BMFillEffect : public BMBase
+{
+public:
+ BMFillEffect() = default;
+ explicit BMFillEffect(const BMFillEffect &other);
+
+ BMBase *clone() const override;
+
+ void construct(const QJsonObject &definition);
+
+ void updateProperties(int frame) override;
+ void render(LottieRenderer &renderer) const override;
+
+ QColor color() const;
+ qreal opacity() const;
+
+protected:
+ BMProperty4D<QVector4D> m_color;
+ BMProperty<qreal> m_opacity;
+};
+
+QT_END_NAMESPACE
+
+#endif // BMFILLEFFECT_P_H
diff --git a/src/bodymovin/bmfreeformshape.cpp b/src/bodymovin/bmfreeformshape.cpp
new file mode 100644
index 0000000..fd5c46b
--- /dev/null
+++ b/src/bodymovin/bmfreeformshape.cpp
@@ -0,0 +1,337 @@
+/****************************************************************************
+**
+** Copyright (C) 2018 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the lottie-qt module of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:GPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 or (at your option) any later version
+** approved by the KDE Free Qt Foundation. The licenses are as published by
+** the Free Software Foundation and appearing in the file LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#include "bmfreeformshape_p.h"
+
+#include <QJsonObject>
+
+#include "bmtrimpath_p.h"
+
+QT_BEGIN_NAMESPACE
+
+BMFreeFormShape::BMFreeFormShape() = default;
+
+BMFreeFormShape::BMFreeFormShape(const BMFreeFormShape &other)
+ : BMShape(other)
+{
+ m_vertexList = other.m_vertexList;
+ m_closedShape = other.m_closedShape;
+ m_vertexMap = other.m_vertexMap;
+}
+
+BMFreeFormShape::BMFreeFormShape(const QJsonObject &definition, BMBase *parent)
+{
+ setParent(parent);
+ construct(definition);
+}
+
+BMBase *BMFreeFormShape::clone() const
+{
+ return new BMFreeFormShape(*this);
+}
+
+void BMFreeFormShape::construct(const QJsonObject &definition)
+{
+ BMBase::parse(definition);
+ if (m_hidden)
+ return;
+
+ qCDebug(lcLottieQtBodymovinParser) << "BMFreeFormShape::construct():" << m_name;
+
+ m_direction = definition.value(QLatin1String("d")).toVariant().toInt();
+
+ QJsonObject vertexObj = definition.value(QLatin1String("ks")).toObject();
+ if (vertexObj.value(QLatin1String("a")).toInt())
+ parseShapeKeyframes(vertexObj);
+ else
+ buildShape(vertexObj.value(QLatin1String("k")).toObject());
+}
+
+void BMFreeFormShape::updateProperties(int frame)
+{
+ if (m_vertexMap.count()) {
+ QJsonObject keyframe = m_vertexMap.value(frame);
+ // If this frame is a keyframe, so values must be updated
+ if (!keyframe.isEmpty())
+ buildShape(keyframe.value(QLatin1String("s")).toArray().at(0).toObject());
+ } else {
+ for (int i =0; i < m_vertexList.count(); i++) {
+ VertexInfo vi = m_vertexList.at(i);
+ vi.pos.update(frame);
+ vi.ci.update(frame);
+ vi.co.update(frame);
+ m_vertexList.replace(i, vi);
+ }
+ buildShape(frame);
+ }
+}
+
+void BMFreeFormShape::render(LottieRenderer &renderer) const
+{
+ renderer.render(*this);
+}
+
+bool BMFreeFormShape::acceptsTrim() const
+{
+ return true;
+}
+
+void BMFreeFormShape::parseShapeKeyframes(QJsonObject &keyframes)
+{
+ QJsonArray vertexKeyframes = keyframes.value(QLatin1String("k")).toArray();
+ for (int i = 0; i < vertexKeyframes.count(); i++) {
+ QJsonObject keyframe = vertexKeyframes.at(i).toObject();
+ if (keyframe.value(QLatin1String("h")).toInt()) {
+ m_vertexMap.insert(keyframe.value(QLatin1String("t")).toVariant().toInt(), keyframe);
+ } else
+ parseEasedVertices(keyframe, keyframe.value(QLatin1String("t")).toVariant().toInt());
+ }
+ if (m_vertexInfos.count())
+ finalizeVertices();
+}
+
+void BMFreeFormShape::buildShape(const QJsonObject &shape)
+{
+ bool needToClose = shape.value(QLatin1String("c")).toBool();
+ QJsonArray bezierIn = shape.value(QLatin1String("i")).toArray();
+ QJsonArray bezierOut = shape.value(QLatin1String("o")).toArray();
+ QJsonArray vertices = shape.value(QLatin1String("v")).toArray();
+
+ // If there are less than two vertices, cannot make a bezier curve
+ if (vertices.count() < 2)
+ return;
+
+ QPointF s(vertices.at(0).toArray().at(0).toDouble(),
+ vertices.at(0).toArray().at(1).toDouble());
+ QPointF s0(s);
+
+ m_path.moveTo(s);
+ int i=0;
+
+ while (i < vertices.count() - 1) {
+ QPointF v = QPointF(vertices.at(i + 1).toArray().at(0).toDouble(),
+ vertices.at(i + 1).toArray().at(1).toDouble());
+ QPointF c1 = QPointF(bezierOut.at(i).toArray().at(0).toDouble(),
+ bezierOut.at(i).toArray().at(1).toDouble());
+ QPointF c2 = QPointF(bezierIn.at(i + 1).toArray().at(0).toDouble(),
+ bezierIn.at(i + 1).toArray().at(1).toDouble());
+ c1 += s;
+ c2 += v;
+
+ m_path.cubicTo(c1, c2, v);
+
+ s = v;
+ i++;
+ }
+
+ if (needToClose) {
+ QPointF v = s0;
+ QPointF c1 = QPointF(bezierOut.at(i).toArray().at(0).toDouble(),
+ bezierOut.at(i).toArray().at(1).toDouble());
+ QPointF c2 = QPointF(bezierIn.at(0).toArray().at(0).toDouble(),
+ bezierIn.at(0).toArray().at(1).toDouble());
+ c1 += s;
+ c2 += v;
+
+ m_path.cubicTo(c1, c2, v);
+ }
+
+ m_path.setFillRule(Qt::WindingFill);
+
+ if (m_direction)
+ m_path = m_path.toReversed();
+}
+
+void BMFreeFormShape::buildShape(int frame)
+{
+ auto it = m_closedShape.constBegin();
+ bool found = false;
+
+ if (frame <= it.key())
+ found = true;
+ else {
+ while (it != m_closedShape.constEnd()) {
+ if (it.key() <= frame) {
+ found = true;
+ break;
+ }
+ ++it;
+ }
+ }
+
+ bool needToClose = false;
+ if (found)
+ needToClose = (*it);
+
+ // If there are less than two vertices, cannot make a bezier curve
+ if (m_vertexList.count() < 2)
+ return;
+
+ QPointF s(m_vertexList.at(0).pos.value());
+ QPointF s0(s);
+
+ m_path.moveTo(s);
+ int i=0;
+
+ while (i < m_vertexList.count() - 1) {
+ QPointF v = m_vertexList.at(i + 1).pos.value();
+ QPointF c1 = m_vertexList.at(i).co.value();
+ QPointF c2 = m_vertexList.at(i + 1).ci.value();
+ c1 += s;
+ c2 += v;
+
+ m_path.cubicTo(c1, c2, v);
+
+ s = v;
+ i++;
+ }
+
+ if (needToClose) {
+ QPointF v = s0;
+ QPointF c1 = m_vertexList.at(i).co.value();
+ QPointF c2 = m_vertexList.at(0).ci.value();
+ c1 += s;
+ c2 += v;
+
+ m_path.cubicTo(c1, c2, v);
+ }
+
+ m_path.setFillRule(Qt::WindingFill);
+
+ if (m_direction)
+ m_path = m_path.toReversed();
+}
+
+void BMFreeFormShape::parseEasedVertices(const QJsonObject &keyframe, int startFrame)
+{
+ QJsonObject startValue = keyframe.value(QLatin1String("s")).toArray().at(0).toObject();
+ QJsonObject endValue = keyframe.value(QLatin1String("e")).toArray().at(0).toObject();
+ bool closedPathAtStart = keyframe.value(QLatin1String("s")).toArray().at(0).toObject().value(QLatin1String("c")).toBool();
+ //bool closedPathAtEnd = keyframe.value(QLatin1String("e")).toArray().at(0).toObject().value(QLatin1String("c")).toBool();
+ QJsonArray startVertices = startValue.value(QLatin1String("v")).toArray();
+ QJsonArray startBezierIn = startValue.value(QLatin1String("i")).toArray();
+ QJsonArray startBezierOut = startValue.value(QLatin1String("o")).toArray();
+ QJsonArray endVertices = endValue.value(QLatin1String("v")).toArray();
+ QJsonArray endBezierIn = endValue.value(QLatin1String("i")).toArray();
+ QJsonArray endBezierOut = endValue.value(QLatin1String("o")).toArray();
+ QJsonObject easingIn = keyframe.value(QLatin1String("i")).toObject();
+ QJsonObject easingOut = keyframe.value(QLatin1String("o")).toObject();
+
+ // if there are no vertices for this keyframe, they keyframe
+ // is the last one, and it must be processed differently
+ if (!startVertices.isEmpty()) {
+ for (int i = 0; i < startVertices.count(); i++) {
+ VertexBuildInfo *buildInfo = m_vertexInfos.value(i, nullptr);
+ if (!buildInfo) {
+ buildInfo = new VertexBuildInfo;
+ m_vertexInfos.insert(i, buildInfo);
+ }
+ QJsonObject posKf = createKeyframe(startVertices.at(i).toArray(),
+ endVertices.at(i).toArray(),
+ startFrame, easingIn, easingOut);
+ buildInfo->posKeyframes.push_back(posKf);
+
+ QJsonObject ciKf = createKeyframe(startBezierIn.at(i).toArray(),
+ endBezierIn.at(i).toArray(),
+ startFrame, easingIn, easingOut);
+ buildInfo->ciKeyframes.push_back(ciKf);
+
+ QJsonObject coKf = createKeyframe(startBezierOut.at(i).toArray(),
+ endBezierOut.at(i).toArray(),
+ startFrame, easingIn, easingOut);
+ buildInfo->coKeyframes.push_back(coKf);
+
+ m_closedShape.insert(startFrame, closedPathAtStart);
+ }
+ } else {
+ // Last keyframe
+
+ int vertexCount = m_vertexInfos.count();
+ for (int i = 0; i < vertexCount; i++) {
+ VertexBuildInfo *buildInfo = m_vertexInfos.value(i, nullptr);
+ if (!buildInfo) {
+ buildInfo = new VertexBuildInfo;
+ m_vertexInfos.insert(i, buildInfo);
+ }
+ QJsonObject posKf;
+ posKf.insert(QLatin1String("t"), startFrame);
+ buildInfo->posKeyframes.push_back(posKf);
+
+ QJsonObject ciKf;
+ ciKf.insert(QLatin1String("t"), startFrame);
+ buildInfo->ciKeyframes.push_back(ciKf);
+
+ QJsonObject coKf;
+ coKf.insert(QLatin1String("t"), startFrame);
+ buildInfo->coKeyframes.push_back(coKf);
+
+ m_closedShape.insert(startFrame, false);
+ }
+ }
+}
+
+void BMFreeFormShape::finalizeVertices()
+{
+
+ for (int i = 0; i < m_vertexInfos.count(); i++) {
+ QJsonObject posObj;
+ posObj.insert(QLatin1String("a"), 1);
+ posObj.insert(QLatin1String("k"), m_vertexInfos.value(i)->posKeyframes);
+
+ QJsonObject ciObj;
+ ciObj.insert(QLatin1String("a"), 1);
+ ciObj.insert(QLatin1String("k"), m_vertexInfos.value(i)->ciKeyframes);
+
+ QJsonObject coObj;
+ coObj.insert(QLatin1String("a"), 1);
+ coObj.insert(QLatin1String("k"), m_vertexInfos.value(i)->coKeyframes);
+
+ VertexInfo vertexInfo;
+ vertexInfo.pos.construct(posObj);
+ vertexInfo.ci.construct(ciObj);
+ vertexInfo.co.construct(coObj);
+ m_vertexList.push_back(vertexInfo);
+ }
+ qDeleteAll(m_vertexInfos);
+}
+
+QJsonObject BMFreeFormShape::createKeyframe(QJsonArray startValue, QJsonArray endValue,
+ int startFrame, QJsonObject easingIn,
+ QJsonObject easingOut)
+{
+ QJsonObject keyframe;
+ keyframe.insert(QLatin1String("t"), startFrame);
+ keyframe.insert(QLatin1String("s"), startValue);
+ keyframe.insert(QLatin1String("e"), endValue);
+ keyframe.insert(QLatin1String("i"), easingIn);
+ keyframe.insert(QLatin1String("o"), easingOut);
+ return keyframe;
+}
+
+QT_END_NAMESPACE
diff --git a/src/bodymovin/bmfreeformshape_p.h b/src/bodymovin/bmfreeformshape_p.h
new file mode 100644
index 0000000..c27e0c6
--- /dev/null
+++ b/src/bodymovin/bmfreeformshape_p.h
@@ -0,0 +1,107 @@
+/****************************************************************************
+**
+** Copyright (C) 2018 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the lottie-qt module of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:GPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 or (at your option) any later version
+** approved by the KDE Free Qt Foundation. The licenses are as published by
+** the Free Software Foundation and appearing in the file LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#ifndef BMFEEFORMSHAPE_P_H
+#define BMFEEFORMSHAPE_P_H
+
+//
+// W A R N I N G
+// -------------
+//
+// This file is not part of the Qt API. It exists purely as an
+// implementation detail. This header file may change from version to
+// version without notice, or even be removed.
+//
+// We mean it.
+//
+
+#include <QPainterPath>
+#include <QJsonArray>
+
+#include <QtBodymovin/bmglobal.h>
+#include <QtBodymovin/private/bmshape_p.h>
+#include <QtBodymovin/private/bmtrimpath_p.h>
+#include <QtBodymovin/private/lottierenderer_p.h>
+
+QT_BEGIN_NAMESPACE
+
+class QJsonObject;
+
+class BODYMOVIN_EXPORT BMFreeFormShape : public BMShape
+{
+public:
+ BMFreeFormShape();
+ explicit BMFreeFormShape(const BMFreeFormShape &other);
+ BMFreeFormShape(const QJsonObject &definition, BMBase *parent = nullptr);
+
+ BMBase *clone() const override;
+
+ void construct(const QJsonObject &definition);
+
+ void updateProperties(int frame) override;
+ void render(LottieRenderer &renderer) const override;
+
+ bool acceptsTrim() const override;
+
+protected:
+ struct VertexInfo {
+ BMProperty2D<QPointF> pos;
+ BMProperty2D<QPointF> ci;
+ BMProperty2D<QPointF> co;
+ };
+
+ void parseShapeKeyframes(QJsonObject &keyframes);
+ void buildShape(const QJsonObject &keyframe);
+ void buildShape(int frame);
+ void parseEasedVertices(const QJsonObject &keyframe, int startFrame);
+
+ QHash<int, QJsonObject> m_vertexMap;
+ QList<VertexInfo> m_vertexList;
+ QMap<int, bool> m_closedShape;
+
+private:
+ struct VertexBuildInfo
+ {
+ QJsonArray posKeyframes;
+ QJsonArray ciKeyframes;
+ QJsonArray coKeyframes;
+ };
+
+ void finalizeVertices();
+
+ QMap<int, VertexBuildInfo*> m_vertexInfos;
+
+ QJsonObject createKeyframe(QJsonArray startValue, QJsonArray endValue,
+ int startFrame, QJsonObject easingIn,
+ QJsonObject easingOut);
+};
+
+QT_END_NAMESPACE
+
+#endif // BMFEEFORMSHAPE_P_H
diff --git a/src/bodymovin/bmgfill.cpp b/src/bodymovin/bmgfill.cpp
new file mode 100644
index 0000000..333e25b
--- /dev/null
+++ b/src/bodymovin/bmgfill.cpp
@@ -0,0 +1,236 @@
+/****************************************************************************
+**
+** Copyright (C) 2018 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the lottie-qt module of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:GPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 or (at your option) any later version
+** approved by the KDE Free Qt Foundation. The licenses are as published by
+** the Free Software Foundation and appearing in the file LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#include "bmgfill_p.h"
+
+#include <QLinearGradient>
+#include <QRadialGradient>
+#include <QtMath>
+#include <QColor>
+
+QT_BEGIN_NAMESPACE
+
+BMGFill::BMGFill(const BMGFill &other)
+ : BMShape(other)
+{
+ m_opacity = other.m_opacity;
+ m_startPoint = other.m_startPoint;
+ m_endPoint = other.m_endPoint;
+ m_highlightLength = other.m_highlightLength;
+ m_highlightAngle = other.m_highlightAngle;
+ m_colors = other.m_colors;
+ if (other.gradientType() == QGradient::LinearGradient)
+ m_gradient = new QLinearGradient;
+ else if (other.gradientType() == QGradient::RadialGradient)
+ m_gradient = new QRadialGradient;
+ else {
+ Q_UNREACHABLE();
+ }
+}
+
+BMGFill::~BMGFill()
+{
+ if (m_gradient)
+ delete m_gradient;
+}
+
+BMBase *BMGFill::clone() const
+{
+ return new BMGFill(*this);
+}
+
+BMGFill::BMGFill(const QJsonObject &definition, BMBase *parent)
+{
+ setParent(parent);
+
+ BMBase::parse(definition);
+ if (m_hidden)
+ return;
+
+ qCDebug(lcLottieQtBodymovinParser) << "BMGFill::construct():" << m_name;
+
+ int type = definition.value(QLatin1String("t")).toVariant().toInt();
+ switch (type) {
+ case 1:
+ m_gradient = new QLinearGradient;
+ break;
+ case 2:
+ m_gradient = new QRadialGradient;
+ break;
+ default:
+ qCWarning(lcLottieQtBodymovinParser) << "Unknown gradient fill type";
+ }
+
+ QJsonObject color = definition.value(QLatin1String("g")).toObject();
+ QJsonArray colorArr = color.value(QLatin1String("k")).toObject().value(QLatin1String("k")).toArray();
+ int elementCount = color.value(QLatin1String("p")).toInt();
+ for (int i = 0; i < (elementCount) * 4; i += 4) {
+ // p denotes the color stop percentage
+ QVector4D colorVec;
+ colorVec[0] = colorArr[i + 1].toVariant().toFloat();
+ colorVec[1] = colorArr[i + 2].toVariant().toFloat();
+ colorVec[2] = colorArr[i + 3].toVariant().toFloat();
+ // Set gradient stop position into w of the vector
+ colorVec[3] = colorArr[i + 0].toVariant().toFloat();
+ BMProperty4D<QVector4D> colorPos;
+ colorPos.setValue(colorVec);
+ m_colors.push_back(colorPos);
+ }
+
+ QJsonObject opacity = definition.value(QLatin1String("o")).toObject();
+ opacity = resolveExpression(opacity);
+ m_opacity.construct(opacity);
+
+ QJsonObject startPoint = definition.value(QLatin1String("s")).toObject();
+ startPoint = resolveExpression(startPoint);
+ m_startPoint.construct(startPoint);
+
+ QJsonObject endPoint = definition.value(QLatin1String("e")).toObject();
+ endPoint = resolveExpression(endPoint);
+ m_endPoint.construct(endPoint);
+
+ QJsonObject highlight = definition.value(QLatin1String("h")).toObject();
+ m_highlightLength.construct(highlight);
+
+ QJsonObject angle = definition.value(QLatin1String("a")).toObject();
+ angle = resolveExpression(angle);
+ m_highlightAngle.construct(angle);
+
+ m_highlightAngle.setValue(0.0);
+}
+
+void BMGFill::updateProperties(int frame)
+{
+ QGradient::Type type = gradientType();
+ if (type != QGradient::LinearGradient &&
+ type != QGradient::RadialGradient)
+ return;
+
+ m_startPoint.update(frame);
+ m_endPoint.update(frame);
+ m_highlightLength.update(frame);
+ m_highlightAngle.update(frame);
+ m_opacity.update(frame);
+ QList<BMProperty4D<QVector4D>>::iterator colorIt = m_colors.begin();
+ while (colorIt != m_colors.end()) {
+ (*colorIt).update(frame);
+ ++colorIt;
+ }
+
+ setGradient();
+}
+
+void BMGFill::render(LottieRenderer &renderer) const
+{
+ renderer.render(*this);
+}
+
+QGradient *BMGFill::value() const
+{
+ return m_gradient;
+}
+
+QGradient::Type BMGFill::gradientType() const
+{
+ if (m_gradient)
+ return m_gradient->type();
+ else
+ return QGradient::NoGradient;
+}
+
+QPointF BMGFill::startPoint() const
+{
+ return m_startPoint.value();
+}
+
+QPointF BMGFill::endPoint() const
+{
+ return m_endPoint.value();
+}
+
+qreal BMGFill::highlightLength() const
+{
+ return m_highlightLength.value();
+}
+
+qreal BMGFill::highlightAngle() const
+{
+ return m_highlightAngle.value();
+}
+
+qreal BMGFill::opacity() const
+{
+ return m_opacity.value();
+}
+
+void BMGFill::setGradient()
+{
+ QList<BMProperty4D<QVector4D>>::iterator colorIt = m_colors.begin();
+ while (colorIt != m_colors.end()) {
+ QVector4D colorPos = (*colorIt).value();
+ QColor color;
+ color.setRedF(static_cast<qreal>(colorPos[0]));
+ color.setGreenF(static_cast<qreal>(colorPos[1]));
+ color.setBlueF(static_cast<qreal>(colorPos[2]));
+ color.setAlphaF(m_opacity.value() / 100.0);
+ m_gradient->setColorAt(static_cast<qreal>(colorPos[3]),
+ color);
+ ++colorIt;
+ }
+
+ switch (gradientType()) {
+ case QGradient::LinearGradient:
+ {
+ QLinearGradient *g = static_cast<QLinearGradient*>(m_gradient);
+ g->setStart(m_startPoint.value());
+ g->setFinalStop(m_endPoint.value());
+ break;
+ }
+ case QGradient::RadialGradient:
+ {
+ QRadialGradient *g = static_cast<QRadialGradient*>(m_gradient);
+ qreal dx = qAbs(m_endPoint.value().x() + m_startPoint.value().x());
+ qreal dy = qAbs(m_endPoint.value().y() + m_startPoint.value().y());
+ qreal radius = qSqrt(dx * dx + dy * dy);
+ qreal angle = qAsin(dy / radius);
+ g->setCenter(m_startPoint.value());
+ g->setCenterRadius(radius);
+ qreal focusRadius = 2;
+ qreal x = (g->radius() - 2 * focusRadius) * qCos(angle + qDegreesToRadians(m_highlightAngle.value()));
+ qreal y = (g->radius() - 2 * focusRadius) * qSin(angle + qDegreesToRadians(m_highlightAngle.value()));
+ g->setFocalPoint(g->center() + QPointF(x, y));
+ g->setFocalRadius(focusRadius);
+ break;
+ }
+ default:
+ break;
+ }
+}
+
+QT_END_NAMESPACE
diff --git a/src/bodymovin/bmgfill_p.h b/src/bodymovin/bmgfill_p.h
new file mode 100644
index 0000000..87fa0f5
--- /dev/null
+++ b/src/bodymovin/bmgfill_p.h
@@ -0,0 +1,91 @@
+/****************************************************************************
+**
+** Copyright (C) 2018 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the lottie-qt module of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:GPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 or (at your option) any later version
+** approved by the KDE Free Qt Foundation. The licenses are as published by
+** the Free Software Foundation and appearing in the file LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#ifndef BMGFILL_P_H
+#define BMGFILL_P_H
+
+//
+// W A R N I N G
+// -------------
+//
+// This file is not part of the Qt API. It exists purely as an
+// implementation detail. This header file may change from version to
+// version without notice, or even be removed.
+//
+// We mean it.
+//
+
+#include <QVector4D>
+#include <QGradient>
+
+#include <QtBodymovin/private/bmgroup_p.h>
+#include <QtBodymovin/private/bmproperty_p.h>
+#include <QtBodymovin/private/bmproperty_p.h>
+#include <QtBodymovin/private/bmspatialproperty_p.h>
+
+QT_BEGIN_NAMESPACE
+
+class BODYMOVIN_EXPORT BMGFill : public BMShape
+{
+public:
+ BMGFill() = default;
+ explicit BMGFill(const BMGFill &other);
+ BMGFill(const QJsonObject &definition, BMBase *parent = nullptr);
+ ~BMGFill() override;
+
+ BMBase *clone() const override;
+
+ void updateProperties(int frame) override;
+ void render(LottieRenderer &renderer) const override;
+
+ QGradient *value() const;
+ QGradient::Type gradientType() const;
+ QPointF startPoint() const;
+ QPointF endPoint() const;
+ qreal highlightLength() const;
+ qreal highlightAngle() const;
+ qreal opacity() const;
+
+private:
+ void setGradient();
+
+protected:
+ BMProperty<qreal> m_opacity;
+ BMSpatialProperty m_startPoint;
+ BMSpatialProperty m_endPoint;
+ BMProperty<qreal> m_highlightLength;
+ BMProperty<qreal> m_highlightAngle;
+ QList<BMProperty4D<QVector4D>> m_colors;
+ QGradient *m_gradient = nullptr;
+
+};
+
+QT_END_NAMESPACE
+
+#endif // BMGFILL_P_H
diff --git a/src/bodymovin/bmglobal.h b/src/bodymovin/bmglobal.h
new file mode 100644
index 0000000..7be60ae
--- /dev/null
+++ b/src/bodymovin/bmglobal.h
@@ -0,0 +1,44 @@
+/****************************************************************************
+**
+** Copyright (C) 2018 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the lottie-qt module of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:GPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 or (at your option) any later version
+** approved by the KDE Free Qt Foundation. The licenses are as published by
+** the Free Software Foundation and appearing in the file LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#ifndef BMGLOBAL_H
+#define BMGLOBAL_H
+
+#include <QtGlobal>
+
+#if defined(BODYMOVIN_LIBRARY)
+# define BODYMOVIN_EXPORT Q_DECL_EXPORT
+#else
+# define BODYMOVIN_EXPORT Q_DECL_IMPORT
+#endif
+
+QT_BEGIN_NAMESPACE
+QT_END_NAMESPACE
+
+#endif // BMGLOBAL_H
diff --git a/src/bodymovin/bmgroup.cpp b/src/bodymovin/bmgroup.cpp
new file mode 100644
index 0000000..126a181
--- /dev/null
+++ b/src/bodymovin/bmgroup.cpp
@@ -0,0 +1,145 @@
+/****************************************************************************
+**
+** Copyright (C) 2018 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the lottie-qt module of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:GPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 or (at your option) any later version
+** approved by the KDE Free Qt Foundation. The licenses are as published by
+** the Free Software Foundation and appearing in the file LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#include "bmgroup_p.h"
+
+#include <QJsonObject>
+#include <QJsonArray>
+
+#include "bmbase_p.h"
+#include "bmshape_p.h"
+#include "bmtrimpath_p.h"
+#include "bmbasictransform_p.h"
+
+QT_BEGIN_NAMESPACE
+
+BMGroup::BMGroup(const QJsonObject &definition, BMBase *parent)
+{
+ setParent(parent);
+ construct(definition);
+}
+
+BMBase *BMGroup::clone() const
+{
+ return new BMGroup(*this);
+}
+
+void BMGroup::construct(const QJsonObject &definition)
+{
+ BMBase::parse(definition);
+ if (m_hidden)
+ return;
+
+ qCDebug(lcLottieQtBodymovinParser) << "BMGroup::construct()"
+ << m_name;
+
+ QJsonArray groupItems = definition.value(QLatin1String("it")).toArray();
+ QJsonArray::const_iterator itemIt = groupItems.constEnd();
+ while (itemIt != groupItems.constBegin()) {
+ itemIt--;
+ BMShape *shape = BMShape::construct((*itemIt).toObject(), this);
+ if (shape) {
+ // Transform affects how group contents are drawn.
+ // It must be traversed first when drawing
+ if (shape->type() == BM_SHAPE_TRANS_IX)
+ addChild(shape, true);
+ else
+ addChild(shape);
+ }
+ }
+}
+
+void BMGroup::updateProperties(int frame)
+{
+ BMShape::updateProperties(frame);
+
+ for (BMBase *child : qAsConst(m_children)) {
+ if (child->hidden())
+ continue;
+
+ BMShape *shape = static_cast<BMShape*>(child);
+ if (shape->type() == BM_SHAPE_TRIM_IX) {
+ BMTrimPath *trim = static_cast<BMTrimPath*>(shape);
+ if (m_appliedTrim)
+ m_appliedTrim->applyTrim(*trim);
+ else
+ m_appliedTrim = trim;
+ } else if (m_appliedTrim && shape->acceptsTrim())
+ shape->applyTrim(*m_appliedTrim);
+ }
+}
+
+void BMGroup::render(LottieRenderer &renderer) const
+{
+ qCDebug(lcLottieQtBodymovinRender) << "Group:" << name();
+
+ renderer.saveState();
+
+ if (m_appliedTrim && !m_appliedTrim->hidden()) {
+ if (m_appliedTrim->simultaneous())
+ renderer.setTrimmingState(LottieRenderer::Simultaneous);
+ else
+ renderer.setTrimmingState(LottieRenderer::Individual);
+ } else
+ renderer.setTrimmingState(LottieRenderer::Off);
+
+ for (BMBase *child : qAsConst(m_children)) {
+ if (child->hidden())
+ continue;
+ child->render(renderer);
+ }
+
+ if (m_appliedTrim && !m_appliedTrim->hidden()
+ && !m_appliedTrim->simultaneous())
+ m_appliedTrim->render(renderer);
+
+ renderer.restoreState();
+}
+
+bool BMGroup::acceptsTrim() const
+{
+ return true;
+}
+
+void BMGroup::applyTrim(const BMTrimPath &trimmer)
+{
+ Q_ASSERT_X(!m_appliedTrim, "BMGroup", "A trim already assigned");
+
+ m_appliedTrim = static_cast<BMTrimPath*>(trimmer.clone());
+ // Setting a friendly name helps in testing
+ m_appliedTrim->setName(QStringLiteral("Inherited from") + trimmer.name());
+
+ for (BMBase *child : qAsConst(m_children)) {
+ BMShape *shape = static_cast<BMShape*>(child);
+ if (shape->acceptsTrim())
+ shape->applyTrim(*m_appliedTrim);
+ }
+}
+
+QT_END_NAMESPACE
diff --git a/src/bodymovin/bmgroup_p.h b/src/bodymovin/bmgroup_p.h
new file mode 100644
index 0000000..1683a1d
--- /dev/null
+++ b/src/bodymovin/bmgroup_p.h
@@ -0,0 +1,76 @@
+/****************************************************************************
+**
+** Copyright (C) 2018 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the lottie-qt module of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:GPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 or (at your option) any later version
+** approved by the KDE Free Qt Foundation. The licenses are as published by
+** the Free Software Foundation and appearing in the file LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#ifndef BMGROUP_P_H
+#define BMGROUP_P_H
+
+//
+// W A R N I N G
+// -------------
+//
+// This file is not part of the Qt API. It exists purely as an
+// implementation detail. This header file may change from version to
+// version without notice, or even be removed.
+//
+// We mean it.
+//
+
+#include <QJsonObject>
+#include <QColor>
+
+#include <QtBodymovin/private/bmshape_p.h>
+#include <QtBodymovin/private/bmproperty_p.h>
+#include <QtBodymovin/private/bmpathtrimmer_p.h>
+
+QT_BEGIN_NAMESPACE
+
+class BMFill;
+class BMTrimPath;
+class BMPathTrimmer;
+
+class BODYMOVIN_EXPORT BMGroup : public BMShape
+{
+public:
+ BMGroup() = default;
+ BMGroup(const QJsonObject &definition, BMBase *parent = nullptr);
+
+ BMBase *clone() const override;
+
+ void construct(const QJsonObject& definition);
+
+ void updateProperties(int frame) override;
+ void render(LottieRenderer &renderer) const override;
+
+ bool acceptsTrim() const override;
+ void applyTrim(const BMTrimPath &trimmer) override;
+};
+
+QT_END_NAMESPACE
+
+#endif // BMGROUP_P_H
diff --git a/src/bodymovin/bmlayer.cpp b/src/bodymovin/bmlayer.cpp
new file mode 100644
index 0000000..5cf781c
--- /dev/null
+++ b/src/bodymovin/bmlayer.cpp
@@ -0,0 +1,279 @@
+/****************************************************************************
+**
+** Copyright (C) 2018 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the lottie-qt module of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:GPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 or (at your option) any later version
+** approved by the KDE Free Qt Foundation. The licenses are as published by
+** the Free Software Foundation and appearing in the file LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#include "bmlayer_p.h"
+
+#include <QJsonArray>
+#include <QJsonObject>
+#include <QJsonValue>
+#include <QLoggingCategory>
+
+#include "bmshapelayer_p.h"
+#include "bmfilleffect_p.h"
+#include "bmbasictransform_p.h"
+
+QT_BEGIN_NAMESPACE
+
+BMLayer::BMLayer(const BMLayer &other)
+ : BMBase(other)
+{
+ m_layerIndex = other.m_layerIndex;
+ m_startFrame = other.m_startFrame;
+ m_endFrame = other.m_endFrame;
+ m_startTime = other.m_startTime;
+ m_blendMode = other.m_blendMode;
+ m_3dLayer = other.m_3dLayer;
+ m_stretch = other.m_stretch;
+ m_parentLayer = other.m_parentLayer;
+ m_td = other.m_td;
+ m_clipMode = other.m_clipMode;
+ if (other.m_effects) {
+ m_effects = new BMBase;
+ for (BMBase *effect : other.m_effects->children())
+ m_effects->addChild(effect->clone());
+ }
+ //m_transformAtFirstFrame = other.m_transformAtFirstFrame;
+}
+
+BMLayer::~BMLayer()
+{
+ if (m_effects)
+ delete m_effects;
+}
+
+BMBase *BMLayer::clone() const
+{
+ return new BMLayer(*this);
+}
+
+BMLayer *BMLayer::construct(QJsonObject definition)
+{
+ qCDebug(lcLottieQtBodymovinParser) << "BMLayer::construct()";
+
+ BMLayer *layer = nullptr;
+ int type = definition.value(QLatin1String("ty")).toInt();
+ switch (type) {
+ case 4:
+ qCDebug(lcLottieQtBodymovinParser) << "Parse shape layer";
+ layer = new BMShapeLayer(definition);
+ break;
+ default:
+ qCWarning(lcLottieQtBodymovinParser) << "Unsupported layer type:" << type;
+ }
+ return layer;
+}
+
+bool BMLayer::active(int frame) const
+{
+ return (!m_hidden && (frame >= m_startFrame && frame <= m_endFrame));
+}
+
+void BMLayer::parse(const QJsonObject &definition)
+{
+ BMBase::parse(definition);
+ if (m_hidden)
+ return;
+
+ qCDebug(lcLottieQtBodymovinParser) << "BMLayer::parse():" << m_name;
+
+ m_layerIndex = definition.value(QLatin1String("ind")).toVariant().toInt();
+ m_startFrame = definition.value(QLatin1String("ip")).toVariant().toInt();
+ m_endFrame = definition.value(QLatin1String("op")).toVariant().toInt();
+ m_startTime = definition.value(QLatin1String("st")).toVariant().toReal();
+ m_blendMode = definition.value(QLatin1String("bm")).toVariant().toInt();
+ m_autoOrient = definition.value(QLatin1String("ao")).toBool();
+ m_3dLayer = definition.value(QLatin1String("ddd")).toBool();
+ m_stretch = definition.value(QLatin1String("sr")).toVariant().toReal();
+ m_parentLayer = definition.value(QLatin1String("parent")).toVariant().toInt();
+ m_td = definition.value(QLatin1String("td")).toInt();
+ int clipMode = definition.value(QLatin1String("tt")).toInt(-1);
+ if (clipMode > -1 && clipMode < 5)
+ m_clipMode = static_cast<MatteClipMode>(clipMode);
+
+ QJsonArray effects = definition.value(QLatin1String("ef")).toArray();
+ parseEffects(effects);
+
+ if (m_td > 1)
+ qCWarning(lcLottieQtBodymovinParser)
+ << "BM Layer: Only alpha mask layer supported:" << m_clipMode;
+ if (m_blendMode > 0)
+ qCWarning(lcLottieQtBodymovinParser)
+ << "BM Layer: Unsupported blend mode" << m_blendMode;
+ if (m_stretch > 1)
+ qCWarning(lcLottieQtBodymovinParser)
+ << "BM Layer: stretch not supported" << m_stretch;
+ if (m_autoOrient)
+ qCWarning(lcLottieQtBodymovinParser)
+ << "BM Layer: auto-orient not supported";
+ if (m_3dLayer)
+ qCWarning(lcLottieQtBodymovinParser)
+ << "BM Layer: is a 3D layer, but not handled";
+}
+
+void BMLayer::updateProperties(int frame)
+{
+ if (m_parentLayer)
+ resolveLinkedLayer();
+
+ // Update first effects, as they are not children of the layer
+ if (m_effects) {
+ for (BMBase* effect : m_effects->children())
+ effect->updateProperties(frame);
+ }
+
+ BMBase::updateProperties(frame);
+}
+
+void BMLayer::render(LottieRenderer &renderer) const
+{
+ // Render first effects, as they affect the children
+ renderEffects(renderer);
+
+ BMBase::render(renderer);
+}
+
+BMBase *BMLayer::findChild(const QString &childName)
+{
+ BMBase *child = nullptr;
+
+ if (m_effects)
+ child = m_effects->findChild(childName);
+
+ if (child)
+ return child;
+ else
+ return BMBase::findChild(childName);
+}
+
+BMLayer *BMLayer::resolveLinkedLayer()
+{
+ if (m_linkedLayer)
+ return m_linkedLayer;
+
+ resolveTopRoot();
+
+ Q_ASSERT(topRoot());
+
+ for (BMBase *child : topRoot()->children()) {
+ BMLayer *layer = static_cast<BMLayer*>(child);
+ if (layer->layerId() == m_parentLayer) {
+ m_linkedLayer = layer;
+ break;
+ }
+ }
+ return m_linkedLayer;
+}
+
+BMLayer *BMLayer::linkedLayer() const
+{
+ return m_linkedLayer;
+}
+
+bool BMLayer::isClippedLayer() const
+{
+ return m_clipMode != NoClip;
+}
+
+bool BMLayer::isMaskLayer() const
+{
+ return m_td > 0;
+}
+
+BMLayer::MatteClipMode BMLayer::clipMode() const
+{
+ return m_clipMode;
+}
+
+int BMLayer::layerId() const
+{
+ return m_layerIndex;
+}
+
+BMBasicTransform *BMLayer::transform() const
+{
+ return m_layerTransform;
+}
+
+void BMLayer::renderEffects(LottieRenderer &renderer) const
+{
+ if (!m_effects)
+ return;
+
+ for (BMBase* effect : m_effects->children()) {
+ if (effect->hidden())
+ continue;
+ effect->render(renderer);
+ }
+}
+
+void BMLayer::parseEffects(const QJsonArray &definition, BMBase *effectRoot)
+{
+ QJsonArray::const_iterator it = definition.constEnd();
+ while (it != definition.constBegin()) {
+ // Create effects container if at least one effect found
+ if (!m_effects) {
+ m_effects = new BMBase;
+ effectRoot = m_effects;
+ }
+ it--;
+ QJsonObject effect = (*it).toObject();
+ int type = effect.value(QLatin1String("ty")).toInt();
+ switch (type) {
+ case 0:
+ {
+ BMBase *slider = new BMBase;
+ slider->parse(effect);
+ effectRoot->addChild(slider);
+ break;
+ }
+ case 5:
+ {
+ if (effect.value(QLatin1String("en")).toInt()) {
+ BMBase *group = new BMBase;
+ group->parse(effect);
+ effectRoot->addChild(group);
+ parseEffects(effect.value(QLatin1String("ef")).toArray(), group);
+ }
+ break;
+ }
+ case 21:
+ {
+ BMFillEffect *fill = new BMFillEffect;
+ fill->construct(effect);
+ effectRoot->addChild(fill);
+ break;
+ }
+ default:
+ qCWarning(lcLottieQtBodymovinParser)
+ << "BMLayer: Unsupported effect" << type;
+ }
+ }
+}
+
+QT_END_NAMESPACE
diff --git a/src/bodymovin/bmlayer_p.h b/src/bodymovin/bmlayer_p.h
new file mode 100644
index 0000000..6f6515f
--- /dev/null
+++ b/src/bodymovin/bmlayer_p.h
@@ -0,0 +1,107 @@
+/****************************************************************************
+**
+** Copyright (C) 2018 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the lottie-qt module of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:GPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 or (at your option) any later version
+** approved by the KDE Free Qt Foundation. The licenses are as published by
+** the Free Software Foundation and appearing in the file LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#ifndef BMLAYER_P_H
+#define BMLAYER_P_H
+
+//
+// W A R N I N G
+// -------------
+//
+// This file is not part of the Qt API. It exists purely as an
+// implementation detail. This header file may change from version to
+// version without notice, or even be removed.
+//
+// We mean it.
+//
+
+#include <QtBodymovin/private/bmbase_p.h>
+
+QT_BEGIN_NAMESPACE
+
+class LottieRenderer;
+
+class BODYMOVIN_EXPORT BMLayer : public BMBase
+{
+public:
+ enum MatteClipMode {NoClip, Alpha, InvertedAlpha, Luminence, InvertedLuminence};
+
+ BMLayer() = default;
+ explicit BMLayer (const BMLayer &other);
+ ~BMLayer() override;
+
+ BMBase *clone() const override;
+
+ static BMLayer *construct(QJsonObject definition);
+
+ bool active(int frame) const override;
+
+ void parse(const QJsonObject &definition) override;
+
+ void updateProperties(int frame) override;
+ void render(LottieRenderer &renderer) const override;
+
+ BMBase *findChild(const QString &childName) override;
+
+ bool isClippedLayer() const;
+ bool isMaskLayer() const;
+ MatteClipMode clipMode() const;
+
+ int layerId() const;
+ BMBasicTransform *transform() const;
+
+protected:
+ void renderEffects(LottieRenderer &renderer) const;
+
+ virtual BMLayer *resolveLinkedLayer();
+ virtual BMLayer *linkedLayer() const;
+
+ int m_layerIndex = 0;
+ int m_startFrame;
+ int m_endFrame;
+ qreal m_startTime;
+ int m_blendMode;
+ bool m_3dLayer = false;
+ BMBase *m_effects = nullptr;
+ qreal m_stretch;
+ BMBasicTransform *m_layerTransform = nullptr;
+
+ int m_parentLayer = 0;
+ int m_td = 0;
+ MatteClipMode m_clipMode = NoClip;
+
+private:
+ void parseEffects(const QJsonArray &definition, BMBase *effectRoot = nullptr);
+
+ BMLayer *m_linkedLayer = nullptr;
+};
+
+QT_END_NAMESPACE
+
+#endif // BMLAYER_P_H
diff --git a/src/bodymovin/bmpathtrimmer.cpp b/src/bodymovin/bmpathtrimmer.cpp
new file mode 100644
index 0000000..c34a67f
--- /dev/null
+++ b/src/bodymovin/bmpathtrimmer.cpp
@@ -0,0 +1,105 @@
+/****************************************************************************
+**
+** Copyright (C) 2018 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the lottie-qt module of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:GPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 or (at your option) any later version
+** approved by the KDE Free Qt Foundation. The licenses are as published by
+** the Free Software Foundation and appearing in the file LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#include "bmpathtrimmer_p.h"
+
+#include "bmtrimpath_p.h"
+#include "lottierenderer_p.h"
+
+#include <QPainterPath>
+
+QT_BEGIN_NAMESPACE
+
+BMPathTrimmer::BMPathTrimmer(BMBase *root)
+ : m_root(root)
+{
+ Q_ASSERT(m_root);
+}
+
+void BMPathTrimmer::addTrim(BMTrimPath* trim)
+{
+ if (!trim)
+ return;
+
+ m_trimPaths.append(trim);
+
+ if (!m_appliedTrim)
+ m_appliedTrim = trim;
+ else
+ qCWarning(lcLottieQtBodymovinParser)
+ << "BM Shape Layer: more than one trim path found on the layer."
+ << "Only one (the first encountered) is supported";
+}
+
+bool BMPathTrimmer::inUse() const
+{
+ return !m_trimPaths.isEmpty();
+}
+
+void BMPathTrimmer::applyTrim(BMShape *shape)
+{
+ if (!m_appliedTrim)
+ return;
+ shape->applyTrim(*m_appliedTrim);
+}
+
+void BMPathTrimmer::updateProperties(int frame)
+{
+ QPainterPath unifiedPath;
+
+ if (m_appliedTrim)
+ m_appliedTrim->updateProperties(frame);
+
+// for (BMBase *child : m_root->children()) {
+// // TODO: Create a better system for recognizing types
+// if (child->type() >= 1000)
+// continue;
+
+// BMShape *shape = static_cast<BMShape*>(child);
+
+// // TODO: Get a better way to inherit trimming
+// if (shape->type() == BM_SHAPE_GROUP_IX && m_appliedTrim)
+// shape->applyTrim(*m_appliedTrim);
+
+// shape->updateProperties(frame);
+
+// if (m_appliedTrim && shape->acceptsTrim())
+// shape->applyTrim(*m_appliedTrim);
+// }
+}
+
+void BMPathTrimmer::render(LottieRenderer &renderer) const
+{
+ Q_UNUSED(renderer);
+// if (m_appliedTrim) {
+// renderer.render(*m_appliedTrim);
+// }
+}
+
+QT_END_NAMESPACE
diff --git a/src/bodymovin/bmpathtrimmer_p.h b/src/bodymovin/bmpathtrimmer_p.h
new file mode 100644
index 0000000..c0ab90b
--- /dev/null
+++ b/src/bodymovin/bmpathtrimmer_p.h
@@ -0,0 +1,78 @@
+/****************************************************************************
+**
+** Copyright (C) 2018 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the lottie-qt module of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:GPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 or (at your option) any later version
+** approved by the KDE Free Qt Foundation. The licenses are as published by
+** the Free Software Foundation and appearing in the file LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+#ifndef BMPATHTRIMMER_P_H
+#define BMPATHTRIMMER_P_H
+
+//
+// W A R N I N G
+// -------------
+//
+// This file is not part of the Qt API. It exists purely as an
+// implementation detail. This header file may change from version to
+// version without notice, or even be removed.
+//
+// We mean it.
+//
+
+#include <QList>
+
+#include <QtBodymovin/bmglobal.h>
+
+QT_BEGIN_NAMESPACE
+
+class QJsonObject;
+class BMTrimPath;
+class LottieRenderer;
+class BMBase;
+class BMShape;
+
+class BODYMOVIN_EXPORT BMPathTrimmer
+{
+public:
+ BMPathTrimmer(BMBase *root);
+
+ void addTrim(BMTrimPath* trim);
+ bool inUse() const;
+
+ void applyTrim(BMShape *shape);
+
+ void updateProperties(int frame);
+ void render(LottieRenderer &renderer) const;
+
+private:
+ BMBase *m_root = nullptr;
+
+ QList<BMTrimPath*> m_trimPaths;
+ BMTrimPath *m_appliedTrim = nullptr;
+};
+
+QT_END_NAMESPACE
+
+#endif // BMPATHTRIMMER_P_H
+
diff --git a/src/bodymovin/bmproperty_p.h b/src/bodymovin/bmproperty_p.h
new file mode 100644
index 0000000..6f00468
--- /dev/null
+++ b/src/bodymovin/bmproperty_p.h
@@ -0,0 +1,395 @@
+/****************************************************************************
+**
+** Copyright (C) 2018 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the lottie-qt module of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:GPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 or (at your option) any later version
+** approved by the KDE Free Qt Foundation. The licenses are as published by
+** the Free Software Foundation and appearing in the file LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#ifndef BMPROPERTY_P_H
+#define BMPROPERTY_P_H
+
+//
+// W A R N I N G
+// -------------
+//
+// This file is not part of the Qt API. It exists purely as an
+// implementation detail. This header file may change from version to
+// version without notice, or even be removed.
+//
+// We mean it.
+//
+
+#include <QJsonObject>
+#include <QJsonArray>
+#include <QJsonValue>
+#include <QPointF>
+#include <QLoggingCategory>
+#include <QtMath>
+
+#include <QDebug>
+
+#include <QtBodymovin/private/bmconstants_p.h>
+#include <QtBodymovin/private/bmlayer_p.h>
+#include <QtBodymovin/private/beziereasing_p.h>
+
+QT_BEGIN_NAMESPACE
+
+template<typename T>
+struct EasingSegment {
+ bool complete = false;
+ double startFrame = 0;
+ double endFrame = 0;
+ T startValue;
+ T endValue;
+ BezierEasing easing;
+};
+
+template<typename T>
+class BODYMOVIN_EXPORT BMProperty
+{
+public:
+ virtual ~BMProperty() = default;
+
+ virtual void construct(const QJsonObject &definition)
+ {
+ if (definition.value(QLatin1String("s")).toVariant().toInt())
+ qCWarning(lcLottieQtBodymovinParser)
+ << "Property is split into separate x and y but it is not supported";
+
+ bool fromExpression = definition.value(QLatin1String("fromExpression")).toBool();
+ m_animated = definition.value(QLatin1String("a")).toDouble() > 0;
+ if (m_animated) {
+ QJsonArray keyframes = definition.value(QLatin1String("k")).toArray();
+ QJsonArray::const_iterator it = keyframes.constBegin();
+ while (it != keyframes.constEnd()) {
+ EasingSegment<T> easing = parseKeyframe((*it).toObject(),
+ fromExpression);
+ addEasing(easing);
+ ++it;
+ }
+ m_value = T();
+ } else
+ m_value = getValue(definition.value(QLatin1String("k")));
+ }
+
+ void setValue(const T& value)
+ {
+ m_value = value;
+ }
+
+ const T& value() const
+ {
+ return m_value;
+ }
+
+ virtual bool update(int frame)
+ {
+ if (!m_animated)
+ return false;
+
+ int adjustedFrame = qBound(m_startFrame, frame, m_endFrame);
+ if (const EasingSegment<T> *easing = getEasingSegment(adjustedFrame)) {
+ qreal progress;
+ if (easing->endFrame == easing->startFrame)
+ progress = 1;
+ else
+ progress = ((adjustedFrame - easing->startFrame) * 1.0) /
+ (easing->endFrame - easing->startFrame);
+ qreal easedValue = easing->easing.valueForProgress(progress);
+ m_value = easing->startValue + easedValue *
+ ((easing->endValue - easing->startValue));
+ return true;
+ }
+ return false;
+ }
+
+protected:
+ void addEasing(EasingSegment<T>& easing)
+ {
+ if (m_easingCurves.length()) {
+ EasingSegment<T> prevEase = m_easingCurves.last();
+ // The end value has to be hand picked to the
+ // previous easing segment, as the json data does
+ // not contain end values for segments
+ prevEase.endFrame = easing.startFrame - 1;
+ m_easingCurves.replace(m_easingCurves.length() - 1, prevEase);
+ }
+ m_easingCurves.push_back(easing);
+ }
+
+ const EasingSegment<T>* getEasingSegment(int frame)
+ {
+ // TODO: Improve with a faster search algorithm
+ const EasingSegment<T> *easing = m_currentEasing;
+ if (!easing || easing->startFrame < frame ||
+ easing->endFrame > frame) {
+ for (int i=0; i < m_easingCurves.length(); i++) {
+ if (m_easingCurves.at(i).startFrame <= frame &&
+ m_easingCurves.at(i).endFrame >= frame) {
+ m_currentEasing = &m_easingCurves.at(i);
+ break;
+ }
+ }
+ }
+
+ if (!m_currentEasing) {
+ qCWarning(lcLottieQtBodymovinParser)
+ << "Property is animated but easing cannot be found";
+ }
+ return m_currentEasing;
+ }
+
+ virtual EasingSegment<T> parseKeyframe(const QJsonObject keyframe,
+ bool fromExpression)
+ {
+ Q_UNUSED(fromExpression);
+
+ EasingSegment<T> easing;
+
+ int startTime = keyframe.value(QLatin1String("t")).toVariant().toInt();
+
+ // AE exported Bodymovin file includes the last
+ // key frame but no other properties.
+ // No need to process in that case
+ if (!keyframe.contains(QLatin1String("s")) && !keyframe.contains(QLatin1String("e"))) {
+ // In this case start time is the last frame for the property
+ this->m_endFrame = startTime;
+ easing.startFrame = startTime;
+ easing.endFrame = startTime;
+ if (m_easingCurves.length()) {
+ easing.startValue = m_easingCurves.last().endValue;
+ easing.endValue = m_easingCurves.last().endValue;
+ }
+ return easing;
+ }
+
+ if (m_startFrame > startTime)
+ m_startFrame = startTime;
+
+ easing.startValue = getValue(keyframe.value(QLatin1String("s")).toArray());
+ easing.endValue = getValue(keyframe.value(QLatin1String("e")).toArray());
+ easing.startFrame = startTime;
+
+ QJsonObject easingIn = keyframe.value(QLatin1String("i")).toObject();
+ QJsonObject easingOut = keyframe.value(QLatin1String("o")).toObject();
+
+ qreal eix = easingIn.value(QLatin1String("x")).toArray().at(0).toDouble();
+ qreal eiy = easingIn.value(QLatin1String("y")).toArray().at(0).toDouble();
+
+ qreal eox = easingOut.value(QLatin1String("x")).toArray().at(0).toDouble();
+ qreal eoy = easingOut.value(QLatin1String("y")).toArray().at(0).toDouble();
+
+ QPointF c1 = QPointF(eox, eoy);
+ QPointF c2 = QPointF(eix, eiy);
+
+ easing.easing.addCubicBezierSegment(c1, c2, QPointF(1.0, 1.0));
+
+ easing.complete = true;
+
+ return easing;
+ }
+
+ virtual T getValue(const QJsonValue &value)
+ {
+ if (value.isArray())
+ return getValue(value.toArray());
+ else {
+ QVariant val = value.toVariant();
+ if (val.canConvert<T>()) {
+ T t = val.value<T>();
+ return t;
+ }
+ else
+ return T();
+ }
+ }
+
+ virtual T getValue(const QJsonArray &value)
+ {
+ QVariant val = value.at(0).toVariant();
+ if (val.canConvert<T>()) {
+ T t = val.value<T>();
+ return t;
+ }
+ else
+ return T();
+ }
+
+protected:
+ bool m_animated = false;
+ QList<EasingSegment<T>> m_easingCurves;
+ const EasingSegment<T> *m_currentEasing = nullptr;
+ int m_startFrame = INT_MAX;
+ int m_endFrame = 0;
+ T m_value;
+};
+
+
+template <typename T>
+class BODYMOVIN_EXPORT BMProperty2D : public BMProperty<T>
+{
+protected:
+ T getValue(const QJsonArray &value) override
+ {
+ if (value.count() > 1)
+ return T(value.at(0).toDouble(),
+ value.at(1).toDouble());
+ else
+ return T();
+ }
+
+ EasingSegment<T> parseKeyframe(const QJsonObject keyframe,
+ bool fromExpression) override
+ {
+ QJsonArray startValues = keyframe.value(QLatin1String("s")).toArray();
+ QJsonArray endValues = keyframe.value(QLatin1String("e")).toArray();
+ int startTime = keyframe.value(QLatin1String("t")).toVariant().toInt();
+
+ EasingSegment<T> easingCurve;
+ easingCurve.startFrame = startTime;
+
+ // AE exported Bodymovin file includes the last
+ // key frame but no other properties.
+ // No need to process in that case
+ if (startValues.isEmpty() && endValues.isEmpty()) {
+ // In this case start time is the last frame for the property
+ this->m_endFrame = startTime;
+ easingCurve.startFrame = startTime;
+ easingCurve.endFrame = startTime;
+ if (this->m_easingCurves.length()) {
+ easingCurve.startValue = this->m_easingCurves.last().endValue;
+ easingCurve.endValue = this->m_easingCurves.last().endValue;
+ }
+ return easingCurve;
+ }
+
+ if (this->m_startFrame > startTime)
+ this->m_startFrame = startTime;
+
+ qreal xs, ys, xe, ye;
+ // Keyframes originating from an expression use only scalar values.
+ // They must be expanded for both x and y coordinates
+ if (fromExpression) {
+ xs = startValues.at(0).toDouble();
+ ys = startValues.at(0).toDouble();
+ xe = endValues.at(0).toDouble();
+ ye = endValues.at(0).toDouble();
+ } else {
+ xs = startValues.at(0).toDouble();
+ ys = startValues.at(1).toDouble();
+ xe = endValues.at(0).toDouble();
+ ye = endValues.at(1).toDouble();
+ }
+ T s(xs, ys);
+ T e(xe, ye);
+
+ QJsonObject easingIn = keyframe.value(QLatin1String("i")).toObject();
+ QJsonObject easingOut = keyframe.value(QLatin1String("o")).toObject();
+
+ easingCurve.startFrame = startTime;
+ easingCurve.startValue = s;
+ easingCurve.endValue = e;
+
+ if (easingIn.value(QLatin1String("x")).isArray()) {
+ QJsonArray eixArr = easingIn.value(QLatin1String("x")).toArray();
+ QJsonArray eiyArr = easingIn.value(QLatin1String("y")).toArray();
+
+ QJsonArray eoxArr = easingOut.value(QLatin1String("x")).toArray();
+ QJsonArray eoyArr = easingOut.value(QLatin1String("y")).toArray();
+
+ while (!eixArr.isEmpty() && !eiyArr.isEmpty()) {
+ qreal eix = eixArr.takeAt(0).toDouble();
+ qreal eiy = eiyArr.takeAt(0).toDouble();
+
+ qreal eox =eoxArr.takeAt(0).toDouble();
+ qreal eoy = eoyArr.takeAt(0).toDouble();
+
+ QPointF c1 = QPointF(eox, eoy);
+ QPointF c2 = QPointF(eix, eiy);
+
+ easingCurve.easing.addCubicBezierSegment(c1, c2, QPointF(1.0, 1.0));
+ }
+ }
+ else {
+ qreal eix = easingIn.value(QLatin1String("x")).toDouble();
+ qreal eiy = easingIn.value(QLatin1String("y")).toDouble();
+
+ qreal eox = easingOut.value(QLatin1String("x")).toDouble();
+ qreal eoy = easingOut.value(QLatin1String("y")).toDouble();
+
+ QPointF c1 = QPointF(eox, eoy);
+ QPointF c2 = QPointF(eix, eiy);
+
+ easingCurve.easing.addCubicBezierSegment(c1, c2, QPointF(1.0, 1.0));
+ }
+
+ easingCurve.complete = true;
+ return easingCurve;
+ }
+};
+
+template <typename T>
+class BODYMOVIN_EXPORT BMProperty4D : public BMProperty<T>
+{
+public:
+ bool update(int frame) override
+ {
+ if (!this->m_animated)
+ return false;
+
+ int adjustedFrame = qBound(this->m_startFrame, frame, this->m_endFrame);
+ if (const EasingSegment<T> *easing = BMProperty<T>::getEasingSegment(adjustedFrame)) {
+ qreal progress = ((adjustedFrame - this->m_startFrame) * 1.0) /
+ (this->m_endFrame - this->m_startFrame);
+ qreal easedValue = easing->easing.valueForProgress(progress);
+ // For the time being, 4D vectors are used only for colors, and
+ // the value must be restricted to between [0, 1]
+ easedValue = qBound(0.0, easedValue, 1.0);
+ T sv = easing->startValue;
+ T ev = easing->endValue;
+ qreal x = sv.x() + easedValue * (ev.x() - sv.x());
+ qreal y = sv.y() + easedValue * (ev.y() - sv.y());
+ qreal z = sv.z() + easedValue * (ev.z() - sv.z());
+ qreal w = sv.w() + easedValue * (ev.w() - sv.w());
+ this->m_value = T(x, y, z, w);
+ }
+
+ return true;
+ }
+
+protected:
+ T getValue(const QJsonArray &value) override
+ {
+ if (value.count() > 3)
+ return T(value.at(0).toDouble(), value.at(1).toDouble(),
+ value.at(2).toDouble(), value.at(3).toDouble());
+ else
+ return T();
+ }
+};
+
+QT_END_NAMESPACE
+
+#endif // BMPROPERTY_P_H
diff --git a/src/bodymovin/bmrect.cpp b/src/bodymovin/bmrect.cpp
new file mode 100644
index 0000000..88909e1
--- /dev/null
+++ b/src/bodymovin/bmrect.cpp
@@ -0,0 +1,137 @@
+/****************************************************************************
+**
+** Copyright (C) 2018 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the lottie-qt module of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:GPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 or (at your option) any later version
+** approved by the KDE Free Qt Foundation. The licenses are as published by
+** the Free Software Foundation and appearing in the file LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#include "bmrect_p.h"
+
+#include <QJsonObject>
+#include <QJsonArray>
+#include <QLoggingCategory>
+
+#include <QDebug>
+
+#include "bmtrimpath_p.h"
+
+QT_BEGIN_NAMESPACE
+
+BMRect::BMRect(const BMRect &other)
+ : BMShape(other)
+{
+ m_position = other.m_position;
+ m_size = other.m_size;
+ m_roundness = other.m_roundness;
+}
+
+BMRect::BMRect(const QJsonObject &definition, BMBase *parent)
+{
+ setParent(parent);
+ BMBase::parse(definition);
+ if (m_hidden)
+ return;
+
+ qCDebug(lcLottieQtBodymovinParser) << "BMRect::BMRect():" << m_name;
+
+ QJsonObject position = definition.value(QLatin1String("p")).toObject();
+ position = resolveExpression(position);
+ m_position.construct(position);
+
+ QJsonObject size = definition.value(QLatin1String("s")).toObject();
+ size = resolveExpression(size);
+ m_size.construct(size);
+
+ QJsonObject roundness = definition.value(QLatin1String("r")).toObject();
+ roundness = resolveExpression(roundness);
+ m_roundness.construct(roundness);
+
+ m_direction = definition.value(QLatin1String("d")).toInt();
+}
+
+
+BMBase *BMRect::clone() const
+{
+ return new BMRect(*this);
+}
+
+bool BMRect::setProperty(BMLiteral::PropertyType propertyType, QVariant value)
+{
+ switch (propertyType) {
+ case BMLiteral::RectPosition:
+ qCDebug(lcLottieQtBodymovinParser) << "Set position" << value.toPointF();
+ m_position.setValue(value.toPointF());
+ break;
+ default:
+ return false;
+ }
+ return true;
+}
+
+void BMRect::updateProperties(int frame)
+{
+ m_size.update(frame);
+ m_position.update(frame);
+ m_roundness.update(frame);
+
+ // AE uses center of a shape as it's position,
+ // in Qt a translation is needed
+ QPointF pos = QPointF(m_position.value().x() - m_size.value().width() / 2,
+ m_position.value().y() - m_size.value().height() / 2);
+
+ m_path = QPainterPath();
+ m_path.addRoundedRect(QRectF(pos, m_size.value()),
+ m_roundness.value(), m_roundness.value());
+
+ if (m_direction)
+ m_path = m_path.toReversed();
+}
+
+void BMRect::render(LottieRenderer &renderer) const
+{
+ renderer.render(*this);
+}
+
+bool BMRect::acceptsTrim() const
+{
+ return true;
+}
+
+QPointF BMRect::position() const
+{
+ return m_position.value();
+}
+
+QSizeF BMRect::size() const
+{
+ return m_size.value();
+}
+
+qreal BMRect::roundness() const
+{
+ return m_roundness.value();
+}
+
+QT_END_NAMESPACE
diff --git a/src/bodymovin/bmrect_p.h b/src/bodymovin/bmrect_p.h
new file mode 100644
index 0000000..924d82f
--- /dev/null
+++ b/src/bodymovin/bmrect_p.h
@@ -0,0 +1,86 @@
+/****************************************************************************
+**
+** Copyright (C) 2018 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the lottie-qt module of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:GPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 or (at your option) any later version
+** approved by the KDE Free Qt Foundation. The licenses are as published by
+** the Free Software Foundation and appearing in the file LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#ifndef BMRECT_P_H
+#define BMRECT_P_H
+
+//
+// W A R N I N G
+// -------------
+//
+// This file is not part of the Qt API. It exists purely as an
+// implementation detail. This header file may change from version to
+// version without notice, or even be removed.
+//
+// We mean it.
+//
+
+#include <QRect>
+#include <QPointF>
+#include <QBrush>
+#include <QPen>
+
+#include <QtBodymovin/private/bmshape_p.h>
+#include <QtBodymovin/private/bmproperty_p.h>
+#include <QtBodymovin/private/bmspatialproperty_p.h>
+#include <QtBodymovin/private/bmfill_p.h>
+#include <QtBodymovin/private/bmstroke_p.h>
+
+QT_BEGIN_NAMESPACE
+
+class BODYMOVIN_EXPORT BMRect : public BMShape
+{
+public:
+ BMRect() = default;
+ explicit BMRect(const BMRect &other);
+ BMRect(const QJsonObject &definition, BMBase *parent = nullptr);
+
+ BMBase *clone() const override;
+
+ void construct(const QJsonObject &definition);
+
+ bool setProperty(BMLiteral::PropertyType propertyType, QVariant value) override;
+
+ void updateProperties(int frame) override;
+ void render(LottieRenderer &renderer) const override;
+ bool acceptsTrim() const override;
+
+ QPointF position() const;
+ QSizeF size() const;
+ qreal roundness() const;
+
+protected:
+ BMSpatialProperty m_position;
+ BMProperty2D<QSizeF> m_size;
+ BMProperty<qreal> m_roundness;
+};
+
+QT_END_NAMESPACE
+
+#endif // BMRECT_P_H
diff --git a/src/bodymovin/bmrepeater.cpp b/src/bodymovin/bmrepeater.cpp
new file mode 100644
index 0000000..f2b45b1
--- /dev/null
+++ b/src/bodymovin/bmrepeater.cpp
@@ -0,0 +1,97 @@
+/****************************************************************************
+**
+** Copyright (C) 2018 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the lottie-qt module of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:GPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 or (at your option) any later version
+** approved by the KDE Free Qt Foundation. The licenses are as published by
+** the Free Software Foundation and appearing in the file LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#include "bmrepeater_p.h"
+
+BMRepeater::BMRepeater(const BMRepeater &other)
+ : BMShape(other)
+{
+ m_copies = other.m_copies;
+ m_offset = other.m_offset;
+ m_transform = m_transform;
+}
+
+BMRepeater::BMRepeater(const QJsonObject &definition, BMBase *parent)
+{
+ setParent(parent);
+ m_transform.setParent(this);
+ construct(definition);
+}
+
+BMBase *BMRepeater::clone() const
+{
+ return new BMRepeater(*this);
+}
+
+void BMRepeater::construct(const QJsonObject &definition)
+{
+ qCDebug(lcLottieQtBodymovinParser) << "BMRepeater::construct():" << m_name;
+
+ BMBase::parse(definition);
+ if (m_hidden)
+ return;
+
+ QJsonObject copies = definition.value(QLatin1String("c")).toObject();
+ copies = resolveExpression(copies);
+ m_copies.construct(copies);
+
+ QJsonObject offset = definition.value(QLatin1String("o")).toObject();
+ offset = resolveExpression(offset);
+ m_offset.construct(offset);
+
+ m_transform.construct(definition.value(QLatin1String("tr")).toObject());
+}
+
+void BMRepeater::updateProperties(int frame)
+{
+ m_copies.update(frame);
+ m_offset.update(frame);
+ m_transform.setInstanceCount(m_copies.value());
+ m_transform.updateProperties(frame);
+}
+
+void BMRepeater::render(LottieRenderer &renderer) const
+{
+ renderer.render(*this);
+}
+
+int BMRepeater::copies() const
+{
+ return m_copies.value();
+}
+
+qreal BMRepeater::offset() const
+{
+ return m_offset.value();
+}
+
+const BMRepeaterTransform &BMRepeater::transform() const
+{
+ return m_transform;
+}
diff --git a/src/bodymovin/bmrepeater_p.h b/src/bodymovin/bmrepeater_p.h
new file mode 100644
index 0000000..de4f9eb
--- /dev/null
+++ b/src/bodymovin/bmrepeater_p.h
@@ -0,0 +1,79 @@
+/****************************************************************************
+**
+** Copyright (C) 2018 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the lottie-qt module of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:GPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 or (at your option) any later version
+** approved by the KDE Free Qt Foundation. The licenses are as published by
+** the Free Software Foundation and appearing in the file LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#ifndef BMREPEATER_P_H
+#define BMREPEATER_P_H
+
+//
+// W A R N I N G
+// -------------
+//
+// This file is not part of the Qt API. It exists purely as an
+// implementation detail. This header file may change from version to
+// version without notice, or even be removed.
+//
+// We mean it.
+//
+
+#include <QtBodymovin/bmglobal.h>
+#include <QtBodymovin/private/bmshape_p.h>
+#include <QtBodymovin/private/bmproperty_p.h>
+#include <QtBodymovin/private/bmrepeatertransform_p.h>
+
+QT_BEGIN_NAMESPACE
+
+class QJsonObject;
+
+class BODYMOVIN_EXPORT BMRepeater : public BMShape
+{
+public:
+ BMRepeater() = default;
+ explicit BMRepeater(const BMRepeater &other);
+ BMRepeater(const QJsonObject &definition, BMBase *parent = nullptr);
+
+ BMBase *clone() const override;
+
+ void construct(const QJsonObject& definition);
+
+ void updateProperties(int frame) override;
+ void render(LottieRenderer &renderer) const override;
+
+ int copies() const;
+ qreal offset() const;
+ const BMRepeaterTransform &transform() const;
+
+protected:
+ BMProperty<int> m_copies;
+ BMProperty<qreal> m_offset;
+ BMRepeaterTransform m_transform;
+};
+
+QT_END_NAMESPACE
+
+#endif // BMREPEATER_P_H
diff --git a/src/bodymovin/bmrepeatertransform.cpp b/src/bodymovin/bmrepeatertransform.cpp
new file mode 100644
index 0000000..1631a3c
--- /dev/null
+++ b/src/bodymovin/bmrepeatertransform.cpp
@@ -0,0 +1,110 @@
+/****************************************************************************
+**
+** Copyright (C) 2018 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the lottie-qt module of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:GPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 or (at your option) any later version
+** approved by the KDE Free Qt Foundation. The licenses are as published by
+** the Free Software Foundation and appearing in the file LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#include "bmrepeatertransform_p.h"
+
+QT_BEGIN_NAMESPACE
+
+BMRepeaterTransform::BMRepeaterTransform(const BMRepeaterTransform &other)
+ : BMBasicTransform(other)
+{
+ m_startOpacity = other.m_startOpacity;
+ m_endOpacity = other.m_endOpacity;
+ m_opacities = other.m_opacities;
+}
+
+BMRepeaterTransform::BMRepeaterTransform(const QJsonObject &definition, BMBase *parent)
+{
+ setParent(parent);
+ construct(definition);
+}
+
+BMBase *BMRepeaterTransform::clone() const
+{
+ return new BMRepeaterTransform(*this);
+}
+
+void BMRepeaterTransform::construct(const QJsonObject &definition)
+{
+ qCDebug(lcLottieQtBodymovinParser) << "BMRepeaterTransform::construct():" << name();
+
+ BMBasicTransform::construct(definition);
+ if (m_hidden)
+ return;
+
+ QJsonObject startOpacity = definition.value(QLatin1String("so")).toObject();
+ startOpacity = resolveExpression(startOpacity);
+ m_startOpacity.construct(startOpacity);
+
+ QJsonObject endOpacity = definition.value(QLatin1String("eo")).toObject();
+ endOpacity = resolveExpression(endOpacity);
+ m_endOpacity.construct(endOpacity);
+}
+
+void BMRepeaterTransform::updateProperties(int frame)
+{
+ BMBasicTransform::updateProperties(frame);
+
+ m_startOpacity.update(frame);
+ m_endOpacity.update(frame);
+
+ m_opacities.clear();
+ for (int i = 0; i < m_copies; i++) {
+ qreal opacity = m_startOpacity.value() +
+ (m_endOpacity.value() - m_startOpacity.value()) * i / m_copies;
+ m_opacities.push_back(opacity);
+ }
+}
+
+void BMRepeaterTransform::render(LottieRenderer &renderer) const
+{
+ renderer.render(*this);
+}
+
+void BMRepeaterTransform::setInstanceCount(int copies)
+{
+ m_copies = copies;
+}
+
+qreal BMRepeaterTransform::opacityAtInstance(int instance) const
+{
+ return m_opacities.at(instance) / 100.0;
+}
+
+qreal BMRepeaterTransform::startOpacity() const
+{
+ return m_startOpacity.value();
+}
+
+qreal BMRepeaterTransform::endOpacity() const
+{
+ return m_endOpacity.value();
+}
+
+QT_END_NAMESPACE
diff --git a/src/bodymovin/bmrepeatertransform_p.h b/src/bodymovin/bmrepeatertransform_p.h
new file mode 100644
index 0000000..259b051
--- /dev/null
+++ b/src/bodymovin/bmrepeatertransform_p.h
@@ -0,0 +1,79 @@
+/****************************************************************************
+**
+** Copyright (C) 2018 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the lottie-qt module of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:GPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 or (at your option) any later version
+** approved by the KDE Free Qt Foundation. The licenses are as published by
+** the Free Software Foundation and appearing in the file LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#ifndef BMREPEATERTRANSFORM_P_H
+#define BMREPEATERTRANSFORM_P_H
+
+//
+// W A R N I N G
+// -------------
+//
+// This file is not part of the Qt API. It exists purely as an
+// implementation detail. This header file may change from version to
+// version without notice, or even be removed.
+//
+// We mean it.
+//
+
+#include <QtBodymovin/private/bmbasictransform_p.h>
+
+QT_BEGIN_NAMESPACE
+
+class QJsonObject;
+
+class BODYMOVIN_EXPORT BMRepeaterTransform : public BMBasicTransform
+{
+public:
+ BMRepeaterTransform() = default;
+ explicit BMRepeaterTransform(const BMRepeaterTransform &other);
+ BMRepeaterTransform(const QJsonObject &definition, BMBase *parent);
+
+ BMBase *clone() const override;
+
+ void construct(const QJsonObject &definition);
+
+ void updateProperties(int frame) override;
+ void render(LottieRenderer &renderer) const override;
+
+ qreal startOpacity() const;
+ qreal endOpacity() const;
+
+ void setInstanceCount(int copies);
+ qreal opacityAtInstance(int instance) const;
+
+protected:
+ int m_copies = 0;
+ BMProperty<qreal> m_startOpacity;
+ BMProperty<qreal> m_endOpacity;
+ QList<qreal> m_opacities;
+};
+
+QT_END_NAMESPACE
+
+#endif // BMREPEATERTRANSFORM_P_H
diff --git a/src/bodymovin/bmround.cpp b/src/bodymovin/bmround.cpp
new file mode 100644
index 0000000..4f02163
--- /dev/null
+++ b/src/bodymovin/bmround.cpp
@@ -0,0 +1,113 @@
+/****************************************************************************
+**
+** Copyright (C) 2018 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the lottie-qt module of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:GPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 or (at your option) any later version
+** approved by the KDE Free Qt Foundation. The licenses are as published by
+** the Free Software Foundation and appearing in the file LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#include "bmround_p.h"
+
+#include <QJsonObject>
+
+#include "bmtrimpath_p.h"
+
+QT_BEGIN_NAMESPACE
+
+BMRound::BMRound(const BMRound &other)
+ : BMShape(other)
+{
+ m_position = other.m_position;
+ m_radius = other.m_radius;
+}
+
+BMRound::BMRound(const QJsonObject &definition, BMBase *parent)
+{
+ setParent(parent);
+ construct(definition);
+}
+
+BMBase *BMRound::clone() const
+{
+ return new BMRound(*this);
+}
+
+void BMRound::construct(const QJsonObject &definition)
+{
+ BMBase::parse(definition);
+ if (m_hidden)
+ return;
+
+ qCDebug(lcLottieQtBodymovinParser) << "BMRound::construct():" << m_name;
+
+ QJsonObject position = definition.value(QLatin1String("p")).toObject();
+ position = resolveExpression(position);
+ m_position.construct(position);
+
+ QJsonObject radius = definition.value(QLatin1String("r")).toObject();
+ radius = resolveExpression(radius);
+ m_radius.construct(radius);
+}
+
+void BMRound::updateProperties(int frame)
+{
+ m_position.update(frame);
+ m_radius.update(frame);
+
+ // AE uses center of a shape as it's position,
+ // in Qt a translation is needed
+ QPointF center = QPointF(m_position.value().x() - m_radius.value() / 2,
+ m_position.value().y() - m_radius.value() / 2);
+
+ m_path = QPainterPath();
+ m_path.arcMoveTo(QRectF(center,
+ QSizeF(m_radius.value(), m_radius.value())), 90);
+ m_path.arcTo(QRectF(center,
+ QSizeF(m_radius.value(), m_radius.value())), 90, -360);
+
+ if (m_direction)
+ m_path = m_path.toReversed();
+}
+
+void BMRound::render(LottieRenderer &renderer) const
+{
+ renderer.render(*this);
+}
+
+bool BMRound::acceptsTrim() const
+{
+ return true;
+}
+
+QPointF BMRound::position() const
+{
+ return m_position.value();
+}
+
+qreal BMRound::radius() const
+{
+ return m_radius.value();
+}
+
+QT_END_NAMESPACE
diff --git a/src/bodymovin/bmround_p.h b/src/bodymovin/bmround_p.h
new file mode 100644
index 0000000..9a66b35
--- /dev/null
+++ b/src/bodymovin/bmround_p.h
@@ -0,0 +1,84 @@
+/****************************************************************************
+**
+** Copyright (C) 2018 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the lottie-qt module of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:GPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 or (at your option) any later version
+** approved by the KDE Free Qt Foundation. The licenses are as published by
+** the Free Software Foundation and appearing in the file LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#ifndef BMROUND_P_H
+#define BMROUND_P_H
+
+//
+// W A R N I N G
+// -------------
+//
+// This file is not part of the Qt API. It exists purely as an
+// implementation detail. This header file may change from version to
+// version without notice, or even be removed.
+//
+// We mean it.
+//
+
+#include <QRect>
+#include <QPointF>
+#include <QBrush>
+#include <QPen>
+
+#include <QtBodymovin/private/bmshape_p.h>
+#include <QtBodymovin/private/bmproperty_p.h>
+#include <QtBodymovin/private/bmspatialproperty_p.h>
+#include <QtBodymovin/private/bmfill_p.h>
+#include <QtBodymovin/private/bmstroke_p.h>
+
+QT_BEGIN_NAMESPACE
+
+class QJsonObject;
+
+class BODYMOVIN_EXPORT BMRound : public BMShape
+{
+public:
+ BMRound() = default;
+ explicit BMRound(const BMRound &other);
+ BMRound(const QJsonObject &definition, BMBase *parent = nullptr);
+
+ BMBase *clone() const override;
+
+ void construct(const QJsonObject &definition);
+
+ void updateProperties(int frame) override;
+ void render(LottieRenderer &renderer) const override;
+ bool acceptsTrim() const override;
+
+ QPointF position() const;
+ qreal radius() const;
+
+protected:
+ BMSpatialProperty m_position;
+ BMProperty<qreal> m_radius;
+};
+
+QT_END_NAMESPACE
+
+#endif // BMROUND_P_H
diff --git a/src/bodymovin/bmshape.cpp b/src/bodymovin/bmshape.cpp
new file mode 100644
index 0000000..098787c
--- /dev/null
+++ b/src/bodymovin/bmshape.cpp
@@ -0,0 +1,205 @@
+/****************************************************************************
+**
+** Copyright (C) 2018 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the lottie-qt module of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:GPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 or (at your option) any later version
+** approved by the KDE Free Qt Foundation. The licenses are as published by
+** the Free Software Foundation and appearing in the file LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#include "bmshape_p.h"
+
+#include <QJsonArray>
+#include <QJsonObject>
+#include <QLoggingCategory>
+
+#include "bmgroup_p.h"
+#include "bmfill_p.h"
+#include "bmgfill_p.h"
+#include "bmstroke_p.h"
+#include "bmrect_p.h"
+#include "bmellipse_p.h"
+#include "bmround_p.h"
+#include "bmtrimpath_p.h"
+#include "bmshapetransform_p.h"
+#include "bmfreeformshape_p.h"
+#include "bmrepeater_p.h"
+#include "bmconstants_p.h"
+
+QT_BEGIN_NAMESPACE
+
+const QMap<QLatin1String, int> BMShape::m_shapeMap =
+ BMShape::setShapeMap();
+
+
+BMShape::BMShape(const BMShape &other)
+ : BMBase(other)
+{
+ m_direction = other.m_direction;
+ m_path = other.m_path;
+ m_appliedTrim = other.m_appliedTrim;
+}
+
+BMBase *BMShape::clone() const
+{
+ return new BMShape(*this);
+}
+
+QMap<QLatin1String, int> BMShape::setShapeMap()
+{
+ QMap<QLatin1String, int> shapeMap;
+ shapeMap.insert(QLatin1String(BM_SHAPE_ELLIPSE_STR), BM_SHAPE_ELLIPSE_IX);
+ shapeMap.insert(QLatin1String(BM_SHAPE_FILL_STR), BM_SHAPE_FILL_IX);
+ shapeMap.insert(QLatin1String(BM_SHAPE_GFILL_STR), BM_SHAPE_GFILL_IX);
+ shapeMap.insert(QLatin1String(BM_SHAPE_GSTROKE_STR), BM_SHAPE_GSTROKE_IX);
+ shapeMap.insert(QLatin1String(BM_SHAPE_GROUP_STR), BM_SHAPE_GROUP_IX);
+ shapeMap.insert(QLatin1String(BM_SHAPE_RECT_STR), BM_SHAPE_RECT_IX);
+ shapeMap.insert(QLatin1String(BM_SHAPE_ROUND_STR), BM_SHAPE_ROUND_IX);
+ shapeMap.insert(QLatin1String(BM_SHAPE_SHAPE_STR), BM_SHAPE_SHAPE_IX);
+ shapeMap.insert(QLatin1String(BM_SHAPE_STAR_STR), BM_SHAPE_STAR_IX);
+ shapeMap.insert(QLatin1String(BM_SHAPE_STROKE_STR), BM_SHAPE_STROKE_IX);
+ shapeMap.insert(QLatin1String(BM_SHAPE_TRIM_STR), BM_SHAPE_TRIM_IX);
+ shapeMap.insert(QLatin1String(BM_SHAPE_TRANSFORM_STR), BM_SHAPE_TRANS_IX);
+ shapeMap.insert(QLatin1String(BM_SHAPE_REPEATER_STR), BM_SHAPE_REPEATER_IX);
+ return shapeMap;
+}
+
+BMShape *BMShape::construct(QJsonObject definition, BMBase *parent, int constructAs)
+{
+ qCDebug(lcLottieQtBodymovinParser) << "BMShape::construct()";
+
+ BMShape *shape = nullptr;
+ QByteArray type = definition.value(QLatin1String("ty")).toVariant().toByteArray();
+
+ int typeToBuild = m_shapeMap.value(QLatin1String(type.data()), -1);
+
+ if (constructAs != BM_SHAPE_ANY_TYPE_IX)
+ typeToBuild = constructAs;
+
+ switch (typeToBuild) {
+ case BM_SHAPE_GROUP_IX:
+ {
+ qCDebug(lcLottieQtBodymovinParser) << "Parse group";
+ shape = new BMGroup(definition, parent);
+ shape->setType(BM_SHAPE_GROUP_IX);
+ break;
+ }
+ case BM_SHAPE_RECT_IX:
+ {
+ qCDebug(lcLottieQtBodymovinParser) << "Parse m_rect";
+ shape = new BMRect(definition, parent);
+ shape->setType(BM_SHAPE_RECT_IX);
+ break;
+ }
+ case BM_SHAPE_FILL_IX:
+ {
+ qCDebug(lcLottieQtBodymovinParser) << "Parse fill";
+ shape = new BMFill(definition, parent);
+ shape->setType(BM_SHAPE_FILL_IX);
+ break;
+ }
+ case BM_SHAPE_GFILL_IX:
+ {
+ qCDebug(lcLottieQtBodymovinParser) << "Parse group fill";
+ shape = new BMGFill(definition, parent);
+ shape->setType(BM_SHAPE_GFILL_IX);
+ break;
+ }
+ case BM_SHAPE_STROKE_IX:
+ {
+ qCDebug(lcLottieQtBodymovinParser) << "Parse stroke";
+ shape = new BMStroke(definition, parent);
+ shape->setType(BM_SHAPE_STROKE_IX);
+ break;
+ }
+ case BM_SHAPE_TRANS_IX:
+ {
+ qCDebug(lcLottieQtBodymovinParser) << "Parse shape transform";
+ shape = new BMShapeTransform(definition, parent);
+ shape->setType(BM_SHAPE_TRANS_IX);
+ break;
+ }
+ case BM_SHAPE_ELLIPSE_IX:
+ {
+ qCDebug(lcLottieQtBodymovinParser) << "Parse ellipse";
+ shape = new BMEllipse(definition);
+ shape->setType(BM_SHAPE_ELLIPSE_IX);
+ break;
+ }
+ case BM_SHAPE_ROUND_IX:
+ {
+ qCDebug(lcLottieQtBodymovinParser) << "Parse round";
+ shape = new BMRound(definition, parent);
+ shape->setType(BM_SHAPE_ELLIPSE_IX);
+ break;
+ }
+ case BM_SHAPE_SHAPE_IX:
+ {
+ qCDebug(lcLottieQtBodymovinParser) << "Parse shape";
+ shape = new BMFreeFormShape(definition, parent);
+ shape->setType(BM_SHAPE_SHAPE_IX);
+ break;
+ }
+ case BM_SHAPE_TRIM_IX:
+ {
+ qCDebug(lcLottieQtBodymovinParser) << "Parse trim path";
+ shape = new BMTrimPath(definition, parent);
+ shape->setType(BM_SHAPE_TRIM_IX);
+ break;
+ }
+ case BM_SHAPE_REPEATER_IX:
+ {
+ qCDebug(lcLottieQtBodymovinParser) << "Parse trim path";
+ shape = new BMRepeater(definition, parent);
+ shape->setType(BM_SHAPE_REPEATER_IX);
+ break;
+ }
+ default:
+ qCWarning(lcLottieQtBodymovinParser) << "Unsupported shape type:"
+ << type;
+ }
+ return shape;
+}
+
+bool BMShape::acceptsTrim() const
+{
+ return false;
+}
+
+void BMShape::applyTrim(const BMTrimPath &trimmer)
+{
+ if (trimmer.simultaneous())
+ m_path = trimmer.trim(m_path);
+}
+
+int BMShape::direction() const
+{
+ return m_direction;
+}
+
+const QPainterPath &BMShape::path() const
+{
+ return m_path;
+}
+
+QT_END_NAMESPACE
diff --git a/src/bodymovin/bmshape_p.h b/src/bodymovin/bmshape_p.h
new file mode 100644
index 0000000..43ca20c
--- /dev/null
+++ b/src/bodymovin/bmshape_p.h
@@ -0,0 +1,100 @@
+/****************************************************************************
+**
+** Copyright (C) 2018 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the lottie-qt module of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:GPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 or (at your option) any later version
+** approved by the KDE Free Qt Foundation. The licenses are as published by
+** the Free Software Foundation and appearing in the file LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#ifndef BMSHAPE_P_H
+#define BMSHAPE_P_H
+
+//
+// W A R N I N G
+// -------------
+//
+// This file is not part of the Qt API. It exists purely as an
+// implementation detail. This header file may change from version to
+// version without notice, or even be removed.
+//
+// We mean it.
+//
+
+#include <QLatin1String>
+#include <QPainterPath>
+
+#include <QtBodymovin/private/bmbase_p.h>
+#include <QtBodymovin/private/bmproperty_p.h>
+
+QT_BEGIN_NAMESPACE
+
+class BMFill;
+class BMStroke;
+class BMTrimPath;
+
+#define BM_SHAPE_ANY_TYPE_IX -1
+#define BM_SHAPE_ELLIPSE_IX 0
+#define BM_SHAPE_FILL_IX 1
+#define BM_SHAPE_GFILL_IX 2
+#define BM_SHAPE_GSTROKE_IX 3
+#define BM_SHAPE_GROUP_IX 4
+#define BM_SHAPE_RECT_IX 5
+#define BM_SHAPE_ROUND_IX 6
+#define BM_SHAPE_SHAPE_IX 7
+#define BM_SHAPE_STAR_IX 8
+#define BM_SHAPE_STROKE_IX 9
+#define BM_SHAPE_TRIM_IX 10
+#define BM_SHAPE_TRANS_IX 11
+#define BM_SHAPE_REPEATER_IX 12
+
+class BODYMOVIN_EXPORT BMShape : public BMBase
+{
+public:
+ BMShape() = default;
+ explicit BMShape(const BMShape &other);
+
+ BMBase *clone() const override;
+
+ static BMShape *construct(QJsonObject definition, BMBase *parent = nullptr, int constructAs = BM_SHAPE_ANY_TYPE_IX);
+
+ virtual const QPainterPath &path() const;
+ virtual bool acceptsTrim() const;
+ virtual void applyTrim(const BMTrimPath& trimmer);
+
+ int direction() const;
+
+protected:
+ QPainterPath m_path;
+ BMTrimPath *m_appliedTrim = nullptr;
+ int m_direction = 0;
+
+private:
+ static QMap<QLatin1String, int> setShapeMap();
+
+ static const QMap<QLatin1String, int> m_shapeMap;
+};
+
+QT_END_NAMESPACE
+
+#endif // BMSHAPE_P_H
diff --git a/src/bodymovin/bmshapelayer.cpp b/src/bodymovin/bmshapelayer.cpp
new file mode 100644
index 0000000..4d50fd2
--- /dev/null
+++ b/src/bodymovin/bmshapelayer.cpp
@@ -0,0 +1,156 @@
+/****************************************************************************
+**
+** Copyright (C) 2018 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the lottie-qt module of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:GPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 or (at your option) any later version
+** approved by the KDE Free Qt Foundation. The licenses are as published by
+** the Free Software Foundation and appearing in the file LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#include "bmshapelayer_p.h"
+
+#include <QJsonObject>
+#include <QJsonArray>
+
+
+#include "bmconstants_p.h"
+#include "bmbase_p.h"
+#include "bmshape_p.h"
+#include "bmtrimpath_p.h"
+#include "bmbasictransform_p.h"
+#include "lottierenderer_p.h"
+
+QT_BEGIN_NAMESPACE
+
+BMShapeLayer::BMShapeLayer(const BMShapeLayer &other)
+ : BMLayer(other)
+{
+ m_maskProperties = other.m_maskProperties;
+ m_layerTransform = new BMBasicTransform(*other.m_layerTransform);
+ m_layerTransform->setParent(this);
+ m_appliedTrim = other.m_appliedTrim;
+}
+
+BMShapeLayer::BMShapeLayer(const QJsonObject &definition)
+{
+ m_type = BM_LAYER_SHAPE_IX;
+
+ BMLayer::parse(definition);
+ if (m_hidden)
+ return;
+
+ qCDebug(lcLottieQtBodymovinParser) << "BMShapeLayer::BMShapeLayer()"
+ << m_name;
+
+ QJsonArray maskProps = definition.value(QLatin1String("maskProperties")).toArray();
+ QJsonArray::const_iterator propIt = maskProps.constBegin();
+ while (propIt != maskProps.constEnd()) {
+ m_maskProperties.append((*propIt).toVariant().toInt());
+ ++propIt;
+ }
+
+ QJsonObject trans = definition.value(QLatin1String("ks")).toObject();
+ m_layerTransform = new BMBasicTransform(trans, this);
+
+ QJsonArray items = definition.value(QLatin1String("shapes")).toArray();
+ QJsonArray::const_iterator itemIt = items.constEnd();
+ while (itemIt != items.constBegin()) {
+ itemIt--;
+ BMShape *shape = BMShape::construct((*itemIt).toObject(), this);
+ if (shape)
+ addChild(shape);
+ }
+
+ if (m_maskProperties.length())
+ qCWarning(lcLottieQtBodymovinParser)
+ << "BM Shape Layer: mask properties found, but not supported"
+ << m_maskProperties;
+}
+
+BMShapeLayer::~BMShapeLayer()
+{
+ if (m_layerTransform)
+ delete m_layerTransform;
+}
+
+BMBase *BMShapeLayer::clone() const
+{
+ return new BMShapeLayer(*this);
+}
+
+void BMShapeLayer::updateProperties(int frame)
+{
+ BMLayer::updateProperties(frame);
+
+ m_layerTransform->updateProperties(frame);
+
+ for (BMBase *child : qAsConst(m_children)) {
+ if (child->hidden())
+ continue;
+
+ BMShape *shape = dynamic_cast<BMShape*>(child);
+
+ if (!shape)
+ continue;
+
+ if (shape->type() == BM_SHAPE_TRIM_IX) {
+ BMTrimPath *trim = static_cast<BMTrimPath*>(shape);
+ if (m_appliedTrim)
+ m_appliedTrim->applyTrim(*trim);
+ else
+ m_appliedTrim = trim;
+ } else if (m_appliedTrim) {
+ if (shape->acceptsTrim())
+ shape->applyTrim(*m_appliedTrim);
+ }
+ }
+}
+
+void BMShapeLayer::render(LottieRenderer &renderer) const
+{
+ renderer.saveState();
+
+ renderEffects(renderer);
+
+ // In case there is a linked layer, apply its transform first
+ // as it affects tranforms of this layer too
+ if (BMLayer *ll = linkedLayer())
+ renderer.render(*ll->transform());
+
+ renderer.render(*this);
+
+ m_layerTransform->render(renderer);
+
+ for (BMBase *child :qAsConst(m_children)) {
+ if (child->hidden())
+ continue;
+ child->render(renderer);
+ }
+
+ if (m_appliedTrim && !m_appliedTrim->hidden())
+ m_appliedTrim->render(renderer);
+
+ renderer.restoreState();
+}
+
+QT_END_NAMESPACE
diff --git a/src/bodymovin/bmshapelayer_p.h b/src/bodymovin/bmshapelayer_p.h
new file mode 100644
index 0000000..babd782
--- /dev/null
+++ b/src/bodymovin/bmshapelayer_p.h
@@ -0,0 +1,77 @@
+/****************************************************************************
+**
+** Copyright (C) 2018 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the lottie-qt module of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:GPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 or (at your option) any later version
+** approved by the KDE Free Qt Foundation. The licenses are as published by
+** the Free Software Foundation and appearing in the file LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#ifndef BMSHAPELAYER_P_H
+#define BMSHAPELAYER_P_H
+
+//
+// W A R N I N G
+// -------------
+//
+// This file is not part of the Qt API. It exists purely as an
+// implementation detail. This header file may change from version to
+// version without notice, or even be removed.
+//
+// We mean it.
+//
+
+#include <QtBodymovin/private/bmlayer_p.h>
+
+QT_BEGIN_NAMESPACE
+
+class QJsonObject;
+
+class LottieRenderer;
+class BMShape;
+class BMTrimPath;
+class BMBasicTransform;
+
+class BODYMOVIN_EXPORT BMShapeLayer : public BMLayer
+{
+public:
+ BMShapeLayer() = default;
+ explicit BMShapeLayer(const BMShapeLayer &other);
+ BMShapeLayer(const QJsonObject &definition);
+ ~BMShapeLayer() override;
+
+ BMBase *clone() const override;
+
+ void updateProperties(int frame) override;
+ void render(LottieRenderer &render) const override;
+
+protected:
+ QList<int> m_maskProperties;
+
+private:
+ BMTrimPath *m_appliedTrim = nullptr;
+};
+
+QT_END_NAMESPACE
+
+#endif // BMSHAPELAYER_P_H
diff --git a/src/bodymovin/bmshapetransform.cpp b/src/bodymovin/bmshapetransform.cpp
new file mode 100644
index 0000000..d9cc24b
--- /dev/null
+++ b/src/bodymovin/bmshapetransform.cpp
@@ -0,0 +1,116 @@
+/****************************************************************************
+**
+** Copyright (C) 2018 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the lottie-qt module of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:GPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 or (at your option) any later version
+** approved by the KDE Free Qt Foundation. The licenses are as published by
+** the Free Software Foundation and appearing in the file LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#include "bmshapetransform_p.h"
+
+#include <QJsonObject>
+#include <QtMath>
+
+#include "bmconstants_p.h"
+#include "bmbasictransform_p.h"
+
+BMShapeTransform::BMShapeTransform(const BMShapeTransform &other)
+ : BMBasicTransform(other)
+{
+ m_skew = other.m_skew;
+ m_skewAxis = other.m_skewAxis;
+ m_shearX = other.m_shearX;
+ m_shearY = other.m_shearY;
+ m_shearAngle = other.m_shearAngle;
+}
+
+BMShapeTransform::BMShapeTransform(const QJsonObject &definition, BMBase *parent)
+{
+ setParent(parent);
+ construct(definition);
+}
+
+BMBase *BMShapeTransform::clone() const
+{
+ return new BMShapeTransform(*this);
+}
+
+void BMShapeTransform::construct(const QJsonObject &definition)
+{
+ BMBasicTransform::construct(definition);
+
+ qCDebug(lcLottieQtBodymovinParser) << "BMShapeTransform::construct():" << BMShape::name();
+
+ QJsonObject skew = definition.value(QLatin1String("sk")).toObject();
+ skew = resolveExpression(skew);
+ m_skew.construct(skew);
+
+ QJsonObject skewAxis = definition.value(QLatin1String("sa")).toObject();
+ skewAxis = resolveExpression(skewAxis);
+ m_skewAxis.construct(skewAxis);
+}
+
+void BMShapeTransform::updateProperties(int frame)
+{
+ BMBasicTransform::updateProperties(frame);
+
+ m_skew.update(frame);
+ m_skewAxis.update(frame);
+
+ double rads = qDegreesToRadians(m_skewAxis.value());
+ m_shearX = qCos(rads);
+ m_shearY = qSin(rads);
+ double tan = qDegreesToRadians(-m_skew.value());
+ m_shearAngle = qTan(tan);
+}
+
+void BMShapeTransform::render(LottieRenderer &renderer) const
+{
+ renderer.render(*this);
+}
+
+qreal BMShapeTransform::skew() const
+{
+ return m_skew.value();
+}
+
+qreal BMShapeTransform::skewAxis() const
+{
+ return m_skewAxis.value();
+}
+
+qreal BMShapeTransform::shearX() const
+{
+ return m_shearX;
+}
+
+qreal BMShapeTransform::shearY() const
+{
+ return m_shearY;
+}
+
+qreal BMShapeTransform::shearAngle() const
+{
+ return m_shearAngle;
+}
diff --git a/src/bodymovin/bmshapetransform_p.h b/src/bodymovin/bmshapetransform_p.h
new file mode 100644
index 0000000..f73f639
--- /dev/null
+++ b/src/bodymovin/bmshapetransform_p.h
@@ -0,0 +1,83 @@
+/****************************************************************************
+**
+** Copyright (C) 2018 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the lottie-qt module of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:GPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 or (at your option) any later version
+** approved by the KDE Free Qt Foundation. The licenses are as published by
+** the Free Software Foundation and appearing in the file LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#ifndef BMSHAPETRANSFORM_P_H
+#define BMSHAPETRANSFORM_P_H
+
+//
+// W A R N I N G
+// -------------
+//
+// This file is not part of the Qt API. It exists purely as an
+// implementation detail. This header file may change from version to
+// version without notice, or even be removed.
+//
+// We mean it.
+//
+
+#include <QPointF>
+
+#include <QtBodymovin/private/bmshape_p.h>
+#include <QtBodymovin/private/bmbasictransform_p.h>
+#include <QtBodymovin/private/bmproperty_p.h>
+
+QT_BEGIN_NAMESPACE
+
+class QJsonObject;
+
+class BODYMOVIN_EXPORT BMShapeTransform : public BMBasicTransform
+{
+public:
+ explicit BMShapeTransform(const BMShapeTransform &other);
+ BMShapeTransform(const QJsonObject &definition, BMBase *parent);
+
+ BMBase *clone() const override;
+
+ void construct(const QJsonObject &definition);
+
+ void updateProperties(int frame) override;
+ void render(LottieRenderer &renderer) const override;
+
+ qreal skew() const;
+ qreal skewAxis() const;
+ qreal shearX() const;
+ qreal shearY() const;
+ qreal shearAngle() const;
+
+protected:
+ BMProperty<qreal> m_skew;
+ BMProperty<qreal> m_skewAxis;
+ qreal m_shearX;
+ qreal m_shearY;
+ qreal m_shearAngle;
+};
+
+QT_END_NAMESPACE
+
+#endif // BMSHAPETRANSFORM_P_H
diff --git a/src/bodymovin/bmspatialproperty_p.h b/src/bodymovin/bmspatialproperty_p.h
new file mode 100644
index 0000000..bb5173d
--- /dev/null
+++ b/src/bodymovin/bmspatialproperty_p.h
@@ -0,0 +1,129 @@
+/****************************************************************************
+**
+** Copyright (C) 2018 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the lottie-qt module of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:GPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 or (at your option) any later version
+** approved by the KDE Free Qt Foundation. The licenses are as published by
+** the Free Software Foundation and appearing in the file LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#ifndef BMSPATIALPROPERTY_P_H
+#define BMSPATIALPROPERTY_P_H
+
+//
+// W A R N I N G
+// -------------
+//
+// This file is not part of the Qt API. It exists purely as an
+// implementation detail. This header file may change from version to
+// version without notice, or even be removed.
+//
+// We mean it.
+//
+
+#include <QPointF>
+#include <QPainterPath>
+
+#include <QtBodymovin/private/bmproperty_p.h>
+
+QT_BEGIN_NAMESPACE
+
+class BMSpatialProperty : public BMProperty2D<QPointF>
+{
+public:
+ virtual void construct(const QJsonObject &definition) override
+ {
+ qCDebug(lcLottieQtBodymovinParser) << "BMSpatialProperty::construct()";
+ BMProperty2D<QPointF>::construct(definition);
+ }
+
+ virtual EasingSegment<QPointF> parseKeyframe(const QJsonObject keyframe, bool fromExpression) override
+ {
+ EasingSegment<QPointF> easing = BMProperty2D<QPointF>::parseKeyframe(keyframe, fromExpression);
+
+ // No need to parse further incomplete keyframes (i.e. last keyframes)
+ if (!easing.complete) {
+ return easing;
+ }
+
+ qreal tix = 0, tiy = 0, tox = 0, toy = 0;
+ if (fromExpression) {
+ // If spatial property definition originates from
+ // an expression (specifically Slider), it contains scalar
+ // property. It must be expanded to both x and y coordinates
+ QJsonArray iArr = keyframe.value(QLatin1String("i")).toArray();
+ QJsonArray oArr = keyframe.value(QLatin1String("o")).toArray();
+
+ if (iArr.count() && oArr.count()) {
+ tix = iArr.at(0).toDouble();
+ tiy = tix;
+ tox = oArr.at(0).toDouble();
+ toy = tox;
+ }
+ } else {
+ QJsonArray tiArr = keyframe.value(QLatin1String("ti")).toArray();
+ QJsonArray toArr = keyframe.value(QLatin1String("to")).toArray();
+
+ if (tiArr.count() && toArr.count()) {
+ tix = tiArr.at(0).toDouble();
+ tiy = tiArr.at(1).toDouble();
+ tox = toArr.at(0).toDouble();
+ toy = toArr.at(1).toDouble();
+ }
+ }
+ QPointF s(easing.startValue);
+ QPointF e(easing.endValue);
+ QPointF c1(tox, toy);
+ QPointF c2(tix, tiy);
+
+ c1 += s;
+ c2 += e;
+
+ m_bezierPath.moveTo(s);
+ m_bezierPath.cubicTo(c1, c2, e);
+
+ return easing;
+ }
+
+ virtual bool update(int frame) override
+ {
+ if (!m_animated)
+ return false;
+
+ int adjustedFrame = qBound(m_startFrame, frame, m_endFrame);
+ if (const EasingSegment<QPointF> *easing = getEasingSegment(adjustedFrame)) {
+ qreal progress = ((adjustedFrame - m_startFrame) * 1.0) / (m_endFrame - m_startFrame);
+ qreal easedValue = easing->easing.valueForProgress(progress);
+ m_value = m_bezierPath.pointAtPercent(easedValue);
+ }
+
+ return true;
+ }
+
+private:
+ QPainterPath m_bezierPath;
+};
+
+QT_END_NAMESPACE
+
+#endif // BMSPATIALPROPERTY_P_H
diff --git a/src/bodymovin/bmstroke.cpp b/src/bodymovin/bmstroke.cpp
new file mode 100644
index 0000000..1d9f16f
--- /dev/null
+++ b/src/bodymovin/bmstroke.cpp
@@ -0,0 +1,151 @@
+/****************************************************************************
+**
+** Copyright (C) 2018 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the lottie-qt module of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:GPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 or (at your option) any later version
+** approved by the KDE Free Qt Foundation. The licenses are as published by
+** the Free Software Foundation and appearing in the file LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#include "bmstroke_p.h"
+
+#include <QLoggingCategory>
+
+#include "bmconstants_p.h"
+
+QT_BEGIN_NAMESPACE
+
+BMStroke::BMStroke(const BMStroke &other)
+ : BMShape(other)
+{
+ m_opacity = other.m_opacity;
+ m_width = other.m_width;
+ m_color = other.m_color;
+ m_capStyle = other.m_capStyle;
+ m_joinStyle = other.m_joinStyle;
+ m_miterLimit = other.m_miterLimit;
+}
+
+BMStroke::BMStroke(const QJsonObject &definition, BMBase *parent)
+{
+ setParent(parent);
+
+ BMBase::parse(definition);
+ if (m_hidden)
+ return;
+
+ qCDebug(lcLottieQtBodymovinParser) << "BMStroke::BMStroke()" << m_name;
+
+ int lineCap = definition.value(QLatin1String("lc")).toVariant().toInt();
+ switch (lineCap) {
+ case 1:
+ m_capStyle = Qt::FlatCap;
+ break;
+ case 2:
+ m_capStyle = Qt::RoundCap;
+ break;
+ case 3:
+ m_capStyle = Qt::SquareCap;
+ break;
+ default:
+ qCDebug(lcLottieQtBodymovinParser) << "Unknown line cap style in BMStroke";
+ }
+
+ int lineJoin = definition.value(QLatin1String("lj")).toVariant().toInt();
+ switch (lineJoin) {
+ case 1:
+ m_joinStyle = Qt::MiterJoin;
+ m_miterLimit = definition.value(QLatin1String("ml")).toVariant().toReal();
+ break;
+ case 2:
+ m_joinStyle = Qt::RoundJoin;
+ break;
+ case 3:
+ m_joinStyle = Qt::BevelJoin;
+ break;
+ default:
+ qCDebug(lcLottieQtBodymovinParser) << "Unknown line join style in BMStroke";
+ }
+
+ QJsonObject opacity = definition.value(QLatin1String("o")).toObject();
+ opacity = resolveExpression(opacity);
+ m_opacity.construct(opacity);
+
+ QJsonObject width = definition.value(QLatin1String("w")).toObject();
+ width = resolveExpression(width);
+ m_width.construct(width);
+
+ QJsonObject color = definition.value(QLatin1String("c")).toObject();
+ color = resolveExpression(color);
+ m_color.construct(color);
+}
+
+BMBase *BMStroke::clone() const
+{
+ return new BMStroke(*this);
+}
+
+void BMStroke::updateProperties(int frame)
+{
+ m_opacity.update(frame);
+ m_width.update(frame);
+ m_color.update(frame);
+}
+
+void BMStroke::render(LottieRenderer &renderer) const
+{
+ renderer.render(*this);
+}
+
+QPen BMStroke::pen() const
+{
+ qreal width = m_width.value();
+ if (qFuzzyIsNull(width))
+ return QPen(Qt::NoPen);
+ QPen pen;
+ pen.setColor(getColor());
+ pen.setWidthF(width);
+ pen.setCapStyle(m_capStyle);
+ pen.setJoinStyle(m_joinStyle);
+ pen.setMiterLimit(m_miterLimit);
+ return pen;
+}
+
+QColor BMStroke::getColor() const
+{
+ QVector4D cVec = m_color.value();
+ QColor color;
+ qreal r = static_cast<qreal>(cVec.x());
+ qreal g = static_cast<qreal>(cVec.y());
+ qreal b = static_cast<qreal>(cVec.z());
+ qreal a = static_cast<qreal>(cVec.w());
+ color.setRgbF(r, g, b, a);
+ return color;
+}
+
+qreal BMStroke::opacity() const
+{
+ return m_opacity.value();
+}
+
+QT_END_NAMESPACE
diff --git a/src/bodymovin/bmstroke_p.h b/src/bodymovin/bmstroke_p.h
new file mode 100644
index 0000000..135d5e5
--- /dev/null
+++ b/src/bodymovin/bmstroke_p.h
@@ -0,0 +1,81 @@
+/****************************************************************************
+**
+** Copyright (C) 2018 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the lottie-qt module of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:GPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 or (at your option) any later version
+** approved by the KDE Free Qt Foundation. The licenses are as published by
+** the Free Software Foundation and appearing in the file LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#ifndef BMSTROKE_P_H
+#define BMSTROKE_P_H
+
+//
+// W A R N I N G
+// -------------
+//
+// This file is not part of the Qt API. It exists purely as an
+// implementation detail. This header file may change from version to
+// version without notice, or even be removed.
+//
+// We mean it.
+//
+
+#include <QPen>
+#include <QVector4D>
+
+#include <QtBodymovin/private/bmshape_p.h>
+#include <QtBodymovin/private/bmproperty_p.h>
+
+QT_BEGIN_NAMESPACE
+
+class BODYMOVIN_EXPORT BMStroke : public BMShape
+{
+public:
+ BMStroke() = default;
+ explicit BMStroke(const BMStroke &other);
+ BMStroke(const QJsonObject &definition, BMBase *parent = nullptr);
+
+ BMBase *clone() const override;
+
+ void updateProperties(int frame) override;
+ void render(LottieRenderer &renderer) const override;
+
+ QPen pen() const;
+ qreal opacity() const;
+
+protected:
+ QColor getColor() const;
+
+protected:
+ BMProperty<qreal> m_opacity;
+ BMProperty<qreal> m_width;
+ BMProperty4D<QVector4D> m_color;
+ Qt::PenCapStyle m_capStyle;
+ Qt::PenJoinStyle m_joinStyle;
+ qreal m_miterLimit;
+};
+
+QT_END_NAMESPACE
+
+#endif // BMSTROKE_P_H
diff --git a/src/bodymovin/bmtrimpath.cpp b/src/bodymovin/bmtrimpath.cpp
new file mode 100644
index 0000000..c5457e9
--- /dev/null
+++ b/src/bodymovin/bmtrimpath.cpp
@@ -0,0 +1,178 @@
+/****************************************************************************
+**
+** Copyright (C) 2018 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the lottie-qt module of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:GPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 or (at your option) any later version
+** approved by the KDE Free Qt Foundation. The licenses are as published by
+** the Free Software Foundation and appearing in the file LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#include "bmtrimpath_p.h"
+
+#include <QtGlobal>
+#include <private/qpainterpath_p.h>
+#include <private/qbezier_p.h>
+
+#include "bmconstants_p.h"
+#include "trimpath_p.h"
+
+BMTrimPath::BMTrimPath()
+{
+ m_appliedTrim = this;
+}
+
+BMTrimPath::BMTrimPath(const QJsonObject &definition, BMBase *parent)
+{
+ m_appliedTrim = this;
+
+ setParent(parent);
+ construct(definition);
+}
+
+BMTrimPath::BMTrimPath(const BMTrimPath &other)
+ : BMShape(other)
+{
+ m_start = other.m_start;
+ m_end = other.m_end;
+ m_offset = other.m_offset;
+ m_simultaneous = other.m_simultaneous;
+}
+
+BMBase *BMTrimPath::clone() const
+{
+ return new BMTrimPath(*this);
+}
+
+void BMTrimPath::construct(const QJsonObject &definition)
+{
+ BMBase::parse(definition);
+ if (m_hidden)
+ return;
+
+ qCDebug(lcLottieQtBodymovinParser) << "BMTrimPath::construct():" << m_name;
+
+ QJsonObject start = definition.value(QLatin1String("s")).toObject();
+ start = resolveExpression(start);
+ m_start.construct(start);
+
+ QJsonObject end = definition.value(QLatin1String("e")).toObject();
+ end = resolveExpression(end);
+ m_end.construct(end);
+
+ QJsonObject offset = definition.value(QLatin1String("o")).toObject();
+ offset = resolveExpression(offset);
+ m_offset.construct(offset);
+
+ int simultaneous = true;
+ if (definition.contains(QLatin1String("m"))) {
+ simultaneous = definition.value(QLatin1String("m")).toInt();
+ }
+ m_simultaneous = (simultaneous == 1);
+
+ if (strcmp(qgetenv("QLOTTIE_FORCE_TRIM_MODE"), "simultaneous") == 0) {
+ qCDebug(lcLottieQtBodymovinRender) << "Forcing trim mode to Simultaneous";
+ m_simultaneous = true;
+ } else if (strcmp(qgetenv("QLOTTIE_FORCE_TRIM_MODE"), "individual") == 0) {
+ qCDebug(lcLottieQtBodymovinRender) << "Forcing trim mode to Individual";
+ m_simultaneous = false;
+ }
+}
+
+void BMTrimPath::updateProperties(int frame)
+{
+ m_start.update(frame);
+ m_end.update(frame);
+ m_offset.update(frame);
+
+ qCDebug(lcLottieQtBodymovinUpdate) << name() << frame << m_start.value()
+ << m_end.value() << m_offset.value();
+
+ BMShape::updateProperties(frame);
+}
+
+void BMTrimPath::render(LottieRenderer &renderer) const
+{
+ if (m_appliedTrim) {
+ if (m_appliedTrim->simultaneous())
+ renderer.setTrimmingState(LottieRenderer::Simultaneous);
+ else
+ renderer.setTrimmingState(LottieRenderer::Individual);
+ } else
+ renderer.setTrimmingState(LottieRenderer::Off);
+
+ renderer.render(*this);
+}
+
+bool BMTrimPath::acceptsTrim() const
+{
+ return true;
+}
+
+void BMTrimPath::applyTrim(const BMTrimPath &other)
+{
+ qCDebug(lcLottieQtBodymovinUpdate) << "Join trim paths:"
+ << other.name() << "into:" << name();
+
+ m_name = m_name + QStringLiteral(" & ") + other.name();
+ qreal newStart = other.start() + (m_start.value() / 100.0) *
+ (other.end() - other.start());
+ qreal newEnd = other.start() + (m_end.value() / 100.0) *
+ (other.end() - other.start());
+
+ m_start.setValue(newStart);
+ m_end.setValue(newEnd);
+ m_offset.setValue(m_offset.value() + other.offset());
+}
+
+qreal BMTrimPath::start() const
+{
+ return m_start.value();
+}
+
+qreal BMTrimPath::end() const
+{
+ return m_end.value();
+}
+
+qreal BMTrimPath::offset() const
+{
+ return m_offset.value();
+}
+
+bool BMTrimPath::simultaneous() const
+{
+ return m_simultaneous;
+}
+
+QPainterPath BMTrimPath::trim(const QPainterPath &path) const
+{
+ TrimPath trimmer;
+ trimmer.setPath(path);
+ qreal offset = m_offset.value() / 360.0;
+ qreal start = m_start.value() / 100.0;
+ qreal end = m_end.value() / 100.0;
+ QPainterPath trimmedPath;
+ if (!qFuzzyIsNull(start - end))
+ trimmedPath = trimmer.trimmed(start, end, offset);
+ return trimmedPath;
+}
diff --git a/src/bodymovin/bmtrimpath_p.h b/src/bodymovin/bmtrimpath_p.h
new file mode 100644
index 0000000..4f4ff42
--- /dev/null
+++ b/src/bodymovin/bmtrimpath_p.h
@@ -0,0 +1,87 @@
+/****************************************************************************
+**
+** Copyright (C) 2018 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the lottie-qt module of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:GPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 or (at your option) any later version
+** approved by the KDE Free Qt Foundation. The licenses are as published by
+** the Free Software Foundation and appearing in the file LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#ifndef BMTRIMPATH_P_H
+#define BMTRIMPATH_P_H
+
+//
+// W A R N I N G
+// -------------
+//
+// This file is not part of the Qt API. It exists purely as an
+// implementation detail. This header file may change from version to
+// version without notice, or even be removed.
+//
+// We mean it.
+//
+
+#include <QPainterPath>
+#include <QJsonObject>
+
+#include <QtBodymovin/private/bmproperty_p.h>
+#include <QtBodymovin/private/bmgroup_p.h>
+
+QT_BEGIN_NAMESPACE
+
+class BODYMOVIN_EXPORT BMTrimPath : public BMShape
+{
+public:
+ BMTrimPath();
+ BMTrimPath(const QJsonObject &definition, BMBase *parent = nullptr);
+ explicit BMTrimPath(const BMTrimPath &other);
+
+ void inherit(const BMTrimPath &other);
+
+ BMBase *clone() const override;
+
+ void construct(const QJsonObject &definition);
+
+ void updateProperties(int frame) override;
+ void render(LottieRenderer &renderer) const override;
+
+ bool acceptsTrim() const override;
+ void applyTrim(const BMTrimPath &trimmer) override;
+
+ qreal start() const;
+ qreal end() const;
+ qreal offset() const;
+ bool simultaneous() const;
+
+ QPainterPath trim(const QPainterPath &path) const;
+
+protected:
+ BMProperty<qreal> m_start;
+ BMProperty<qreal> m_end;
+ BMProperty<qreal> m_offset;
+ bool m_simultaneous = false;
+};
+
+QT_END_NAMESPACE
+
+#endif // BMTRIMPATH_P_H
diff --git a/src/bodymovin/bodymovin.pro b/src/bodymovin/bodymovin.pro
new file mode 100644
index 0000000..c50cad8
--- /dev/null
+++ b/src/bodymovin/bodymovin.pro
@@ -0,0 +1,79 @@
+#-------------------------------------------------
+#
+# Project created by QtCreator 2018-09-28T16:59:29
+#
+#-------------------------------------------------
+
+TARGET = QtBodymovin
+MODULE = bodymovin
+
+CONFIG += internal_module
+
+QT += gui-private
+
+DEFINES += BODYMOVIN_LIBRARY
+
+# The following define makes your compiler emit warnings if you use
+# any feature of Qt which has been marked as deprecated (the exact warnings
+# depend on your compiler). Please consult the documentation of the
+# deprecated API in order to know how to port your code away from it.
+DEFINES += QT_DEPRECATED_WARNINGS
+
+# You can also make your code fail to compile if you use deprecated APIs.
+# In order to do so, uncomment the following line.
+# You can also select to disable deprecated APIs only up to a certain version of Qt.
+#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0
+
+SOURCES += \
+ bmbase.cpp \
+ bmlayer.cpp \
+ bmshape.cpp \
+ bmshapelayer.cpp \
+ bmrect.cpp \
+ bmfill.cpp \
+ bmgfill.cpp \
+ bmgroup.cpp \
+ bmstroke.cpp \
+ bmbasictransform.cpp \
+ bmshapetransform.cpp \
+ bmellipse.cpp \
+ bmround.cpp \
+ bmfreeformshape.cpp \
+ bmtrimpath.cpp \
+ bmpathtrimmer.cpp \
+ lottierenderer.cpp \
+ trimpath.cpp \
+ bmfilleffect.cpp \
+ bmrepeater.cpp \
+ bmrepeatertransform.cpp \
+ beziereasing.cpp
+
+HEADERS += \
+ beziereasing_p.h \
+ bmbase_p.h \
+ bmbasictransform_p.h \
+ bmconstants_p.h \
+ bmellipse_p.h \
+ bmfill_p.h \
+ bmfilleffect_p.h \
+ bmfreeformshape_p.h \
+ bmgfill_p.h \
+ bmgroup_p.h \
+ bmlayer_p.h \
+ bmproperty_p.h \
+ bmrect_p.h \
+ bmrepeater_p.h \
+ bmrepeatertransform_p.h \
+ bmround_p.h \
+ bmshape_p.h \
+ bmshapelayer_p.h \
+ bmshapetransform_p.h \
+ bmspatialproperty_p.h \
+ bmstroke_p.h \
+ bmtrimpath_p.h \
+ trimpath_p.h \
+ lottierenderer_p.h \
+ bmpathtrimmer_p.h \
+ bmglobal.h
+
+load(qt_module)
diff --git a/src/bodymovin/lottierenderer.cpp b/src/bodymovin/lottierenderer.cpp
new file mode 100644
index 0000000..c08eb46
--- /dev/null
+++ b/src/bodymovin/lottierenderer.cpp
@@ -0,0 +1,55 @@
+/****************************************************************************
+**
+** Copyright (C) 2018 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the lottie-qt module of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:GPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 or (at your option) any later version
+** approved by the KDE Free Qt Foundation. The licenses are as published by
+** the Free Software Foundation and appearing in the file LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#include "lottierenderer_p.h"
+
+QT_BEGIN_NAMESPACE
+
+void LottieRenderer::setTrimmingState(LottieRenderer::TrimmingState trimmingState)
+{
+ m_trimmingState = trimmingState;
+}
+
+LottieRenderer::TrimmingState LottieRenderer::trimmingState() const
+{
+ return m_trimmingState;
+}
+
+void LottieRenderer::saveTrimmingState()
+{
+ m_trimStateStack.push(m_trimmingState);
+}
+
+void LottieRenderer::restoreTrimmingState()
+{
+ if (m_trimStateStack.count())
+ m_trimmingState = m_trimStateStack.pop();
+}
+
+QT_END_NAMESPACE
diff --git a/src/bodymovin/lottierenderer_p.h b/src/bodymovin/lottierenderer_p.h
new file mode 100644
index 0000000..4089e8a
--- /dev/null
+++ b/src/bodymovin/lottierenderer_p.h
@@ -0,0 +1,107 @@
+/****************************************************************************
+**
+** Copyright (C) 2018 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the lottie-qt module of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:GPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 or (at your option) any later version
+** approved by the KDE Free Qt Foundation. The licenses are as published by
+** the Free Software Foundation and appearing in the file LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#ifndef LOTTIERENDERER_H
+#define LOTTIERENDERER_H
+
+//
+// W A R N I N G
+// -------------
+//
+// This file is not part of the Qt API. It exists purely as an
+// implementation detail. This header file may change from version to
+// version without notice, or even be removed.
+//
+// We mean it.
+//
+
+#include <QStack>
+
+#include "bmglobal.h"
+
+QT_BEGIN_NAMESPACE
+
+class BMBase;
+class BMLayer;
+class BMRect;
+class BMFill;
+class BMGFill;
+class BMStroke;
+class BMBasicTransform;
+class BMLayerTransform;
+class BMShapeTransform;
+class BMRepeaterTransform;
+class BMShapeLayer;
+class BMEllipse;
+class BMRound;
+class BMFreeFormShape;
+class BMTrimPath;
+class BMFillEffect;
+class BMRepeater;
+
+class BODYMOVIN_EXPORT LottieRenderer
+{
+public:
+ enum TrimmingState{Off = 0, Simultaneous, Individual};
+
+ virtual ~LottieRenderer() = default;
+
+ virtual void saveState() = 0;
+ virtual void restoreState() = 0;
+
+ virtual void setTrimmingState(TrimmingState state);
+ virtual TrimmingState trimmingState() const;
+
+ virtual void render(const BMLayer &layer) = 0;
+ virtual void render(const BMRect &rect) = 0;
+ virtual void render(const BMEllipse &ellipse) = 0;
+ virtual void render(const BMRound &round) = 0;
+ virtual void render(const BMFill &fill) = 0;
+ virtual void render(const BMGFill &fill) = 0;
+ virtual void render(const BMStroke &stroke) = 0;
+ virtual void render(const BMBasicTransform &trans) = 0;
+ virtual void render(const BMShapeTransform &trans) = 0;
+ virtual void render(const BMFreeFormShape &shape) = 0;
+ virtual void render(const BMTrimPath &trans) = 0;
+ virtual void render(const BMFillEffect &effect) = 0;
+ virtual void render(const BMRepeater &repeater) = 0;
+
+protected:
+ void saveTrimmingState();
+ void restoreTrimmingState();
+
+ TrimmingState m_trimmingState = Off;
+
+private:
+ QStack<LottieRenderer::TrimmingState> m_trimStateStack;
+};
+
+QT_END_NAMESPACE
+
+#endif // LOTTIERENDERER_H
diff --git a/src/bodymovin/trimpath.cpp b/src/bodymovin/trimpath.cpp
new file mode 100644
index 0000000..1cd124b
--- /dev/null
+++ b/src/bodymovin/trimpath.cpp
@@ -0,0 +1,226 @@
+/****************************************************************************
+**
+** Copyright (C) 2018 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the lottie-qt module of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:GPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 or (at your option) any later version
+** approved by the KDE Free Qt Foundation. The licenses are as published by
+** the Free Software Foundation and appearing in the file LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#include <trimpath_p.h>
+#include <private/qpainterpath_p.h>
+#include <private/qbezier_p.h>
+#include <QtMath>
+
+QT_BEGIN_NAMESPACE
+
+/*
+Returns the path trimmed to length fractions f1, f2, in range [0.0, 1.0].
+f1 and f2 are displaced, with wrapping, by the fractional part of offset, effective range <-1.0, 1.0>
+*/
+QPainterPath TrimPath::trimmed(qreal f1, qreal f2, qreal offset) const
+{
+ QPainterPath res;
+ if (mPath.isEmpty() || !mPath.elementAt(0).isMoveTo())
+ return res;
+
+ f1 = qBound(0.0, f1, 1.0);
+ f2 = qBound(0.0, f2, 1.0);
+ if (qFuzzyCompare(f1, f2))
+ return res;
+ if (f1 > f2)
+ qSwap(f1, f2);
+ if (qFuzzyCompare(f2 - f1, 1.0)) // Shortcut for no trimming
+ return mPath;
+
+ qreal dummy;
+ offset = modf(offset, &dummy); // Use only the fractional part of offset, range <-1, 1>
+
+ qreal of1 = f1 + offset;
+ qreal of2 = f2 + offset;
+ if (offset < 0.0) {
+ f1 = of1 < 0.0 ? of1 + 1.0 : of1;
+ f2 = of2 + 1.0 > 1.0 ? of2 : of2 + 1.0;
+ } else if (offset > 0.0) {
+ f1 = of1 - 1.0 < 0.0 ? of1 : of1 - 1.0;
+ f2 = of2 > 1.0 ? of2 - 1.0 : of2;
+ }
+ bool wrapping = (f1 > f2);
+ //qDebug() << "ADJ:" << f1 << f2 << wrapping << "(" << of1 << of2 << ")";
+
+ if (lensIsDirty())
+ updateLens();
+ qreal totLen = mLens.last();
+ if (qFuzzyIsNull(totLen))
+ return res;
+
+ qreal l1 = f1 * totLen;
+ qreal l2 = f2 * totLen;
+ const int e1 = elementAtLength(l1);
+ const bool mustTrimE1 = !qFuzzyCompare(mLens.at(e1), l1);
+ const int e2 = elementAtLength(l2);
+ const bool mustTrimE2 = !qFuzzyCompare(mLens.at(e2), l2);
+
+ //qDebug() << "Trim [" << f1 << f2 << "] e1:" << e1 << mustTrimE1 << "e2:" << e2 << mustTrimE2 << "wrapping:" << wrapping;
+
+ if (e1 == e2 && !wrapping && mustTrimE1 && mustTrimE2) {
+ // Entire result is one element, clipped in both ends
+ appendTrimmedElement(&res, e1, true, l1, true, l2);
+ } else {
+ // Partial start element, or just its end point
+ if (mustTrimE1)
+ appendEndOfElement(&res, e1, l1);
+ else
+ res.moveTo(endPointOfElement(e1));
+
+ // Complete elements between start and end
+ if (wrapping) {
+ appendElementRange(&res, e1 + 1, mPath.elementCount() - 1);
+ res.moveTo(mPath.elementAt(0));
+ appendElementRange(&res, 1, (mustTrimE2 ? e2 - 1 : e2));
+ } else {
+ appendElementRange(&res, e1 + 1, (mustTrimE2 ? e2 - 1 : e2));
+ }
+
+ // Partial end element
+ if (mustTrimE2)
+ appendStartOfElement(&res, e2, l2);
+ }
+ return res;
+}
+
+void TrimPath::updateLens() const
+{
+ const int numElems = mPath.elementCount();
+ mLens.resize(numElems);
+ if (!numElems)
+ return;
+
+ QPointF runPt = mPath.elementAt(0);
+ qreal runLen = 0.0;
+ for (int i = 0; i < numElems; i++) {
+ QPainterPath::Element e = mPath.elementAt(i);
+ switch (e.type) {
+ case QPainterPath::LineToElement:
+ runLen += QLineF(runPt, e).length();
+ runPt = e;
+ break;
+ case QPainterPath::CurveToElement: {
+ Q_ASSERT(i < numElems - 2);
+ QPainterPath::Element ee = mPath.elementAt(i + 2);
+ runLen += QBezier::fromPoints(runPt, e, mPath.elementAt(i + 1), ee).length();
+ runPt = ee;
+ break;
+ }
+ case QPainterPath::MoveToElement:
+ runPt = e;
+ break;
+ case QPainterPath::CurveToDataElement:
+ break;
+ }
+ mLens[i] = runLen;
+ }
+}
+
+int TrimPath::elementAtLength(qreal len) const
+{
+ const auto it = std::lower_bound(mLens.constBegin(), mLens.constEnd(), len);
+ return (it == mLens.constEnd()) ? mLens.size() - 1 : int(it - mLens.constBegin());
+}
+
+QPointF TrimPath::endPointOfElement(int elemIdx) const
+{
+ QPainterPath::Element e = mPath.elementAt(elemIdx);
+ if (e.isCurveTo())
+ return mPath.elementAt(qMin(elemIdx + 2, mPath.elementCount() - 1));
+ else
+ return e;
+}
+
+void TrimPath::appendTrimmedElement(QPainterPath *to, int elemIdx, bool trimStart, qreal startLen, bool trimEnd, qreal endLen) const
+{
+ Q_ASSERT(elemIdx > 0);
+
+ if (lensIsDirty())
+ updateLens();
+
+ qreal prevLen = mLens.at(elemIdx - 1);
+ qreal elemLen = mLens.at(elemIdx) - prevLen;
+ qreal len1 = startLen - prevLen;
+ qreal len2 = endLen - prevLen;
+ if (qFuzzyIsNull(elemLen))
+ return;
+
+ QPointF pp = mPath.elementAt(elemIdx - 1);
+ QPainterPath::Element e = mPath.elementAt(elemIdx);
+ if (e.isLineTo()) {
+ QLineF l(pp, e);
+ QPointF p1 = trimStart ? l.pointAt(len1 / elemLen) : pp;
+ QPointF p2 = trimEnd ? l.pointAt(len2 / elemLen) : e;
+ if (to->isEmpty())
+ to->moveTo(p1);
+ to->lineTo(p2);
+ } else if (e.isCurveTo()) {
+ Q_ASSERT(elemIdx < mPath.elementCount() - 2);
+
+ QBezier b = QBezier::fromPoints(pp, e, mPath.elementAt(elemIdx + 1), mPath.elementAt(elemIdx + 2));
+ qreal t1 = trimStart ? b.tAtLength(len1) : 0.0; // or simply len1/elemLen to trim by t instead of len
+ qreal t2 = trimEnd ? b.tAtLength(len2) : 1.0;
+ QBezier c = b.getSubRange(t1, t2);
+ if (to->isEmpty())
+ to->moveTo(c.pt1());
+ to->cubicTo(c.pt2(), c.pt3(), c.pt4());
+ }
+ else {
+ Q_UNREACHABLE();
+ }
+}
+
+void TrimPath::appendElementRange(QPainterPath *to, int first, int last) const
+{
+ //# (in QPPP, could do direct vector copy, better performance)
+ if (first >= mPath.elementCount() || last >= mPath.elementCount())
+ return;
+
+ for (int i = first; i <= last; i++) {
+ QPainterPath::Element e = mPath.elementAt(i);
+ switch (e.type) {
+ case QPainterPath::MoveToElement:
+ to->moveTo(e);
+ break;
+ case QPainterPath::LineToElement:
+ to->lineTo(e);
+ break;
+ case QPainterPath::CurveToElement:
+ Q_ASSERT(i < mPath.elementCount() - 2);
+ to->cubicTo(e, mPath.elementAt(i + 1), mPath.elementAt(i + 2));
+ i += 2;
+ break;
+ default:
+ // 'first' may point to CurveToData element, just skip it
+ break;
+ }
+ }
+}
+
+QT_END_NAMESPACE
diff --git a/src/bodymovin/trimpath_p.h b/src/bodymovin/trimpath_p.h
new file mode 100644
index 0000000..5aac2d0
--- /dev/null
+++ b/src/bodymovin/trimpath_p.h
@@ -0,0 +1,93 @@
+/****************************************************************************
+**
+** Copyright (C) 2018 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the lottie-qt module of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:GPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 or (at your option) any later version
+** approved by the KDE Free Qt Foundation. The licenses are as published by
+** the Free Software Foundation and appearing in the file LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#ifndef TRIMPATH_P_H
+#define TRIMPATH_P_H
+
+//
+// W A R N I N G
+// -------------
+//
+// This file is not part of the Qt API. It exists purely as an
+// implementation detail. This header file may change from version to
+// version without notice, or even be removed.
+//
+// We mean it.
+//
+
+#include <QPainterPath>
+
+QT_BEGIN_NAMESPACE
+
+class TrimPath {
+public:
+ TrimPath() = default;
+ TrimPath(const QPainterPath &path)
+ : mPath(path) {}
+ TrimPath(const TrimPath &other)
+ : mPath(other.mPath), mLens(other.mLens) {}
+ ~TrimPath() {}
+
+ void setPath(const QPainterPath &path) {
+ mPath = path;
+ mLens.clear();
+ }
+
+ QPainterPath path() const {
+ return mPath;
+ }
+
+ QPainterPath trimmed(qreal f1, qreal f2, qreal offset = 0.0) const;
+
+private:
+ bool lensIsDirty() const {
+ return mLens.size() != mPath.elementCount();
+ }
+ void updateLens() const;
+ int elementAtLength(qreal len) const;
+ QPointF endPointOfElement(int elemIdx) const;
+ void appendTrimmedElement(QPainterPath *to, int elemIdx, bool trimStart, qreal startLen, bool trimEnd, qreal endLen) const;
+ void appendStartOfElement(QPainterPath *to, int elemIdx, qreal len) const {
+ appendTrimmedElement(to, elemIdx, false, 0.0, true, len);
+ }
+ void appendEndOfElement(QPainterPath *to, int elemIdx, qreal len) const {
+ appendTrimmedElement(to, elemIdx, true, len, false, 1.0);
+ }
+
+ void appendElementRange(QPainterPath *to, int first, int last) const;
+
+ QPainterPath mPath;
+ mutable QVector<qreal> mLens;
+};
+
+QT_END_NAMESPACE
+
+Q_DECLARE_METATYPE(TrimPath);
+
+#endif // TRIMPATH_P_H
diff --git a/src/imports/doc/lottieanimation.qdocconf b/src/imports/doc/lottieanimation.qdocconf
new file mode 100644
index 0000000..fc7e25f
--- /dev/null
+++ b/src/imports/doc/lottieanimation.qdocconf
@@ -0,0 +1,14 @@
+include($QT_INSTALL_DOCS/global/qt-html-templates-offline.qdocconf)
+include($QT_INSTALL_DOCS/global/fileextensions.qdocconf)
+
+project = lottieqt
+description = Bodymovin player for Qt
+
+moduleheader = lottieanimationdoc
+includepaths = -I .
+
+outputdir = html
+
+headerdirs += ../..
+
+sourcedirs += ../..
diff --git a/src/imports/doc/lottieanimationdoc b/src/imports/doc/lottieanimationdoc
new file mode 100644
index 0000000..718bfa2
--- /dev/null
+++ b/src/imports/doc/lottieanimationdoc
@@ -0,0 +1 @@
+#include "../lottieanimation.h"
diff --git a/src/imports/imports.pro b/src/imports/imports.pro
new file mode 100644
index 0000000..8d75727
--- /dev/null
+++ b/src/imports/imports.pro
@@ -0,0 +1,24 @@
+CXX_MODULE = qtlottie
+TARGET = lottieqt
+TARGETPATH = Qt/labs/lottieqt
+IMPORT_VERSION = 1.$$QT_MINOR_VERSION
+
+QT += qml quick gui-private bodymovin-private
+CONFIG += plugin c++11
+
+QMAKE_DOCS = $$PWD/doc/lottieanimation.qdocconf
+
+# Input
+SOURCES += \
+ lottieanimation.cpp \
+ lottie_plugin.cpp \
+ rasterrenderer/lottierasterrenderer.cpp \
+ rasterrenderer/batchrenderer.cpp
+
+HEADERS += \
+ lottieanimation.h \
+ lottie_plugin.h \
+ rasterrenderer/lottierasterrenderer.h \
+ rasterrenderer/batchrenderer.h
+
+load(qml_plugin)
diff --git a/src/imports/lottie_plugin.cpp b/src/imports/lottie_plugin.cpp
new file mode 100644
index 0000000..e5886c7
--- /dev/null
+++ b/src/imports/lottie_plugin.cpp
@@ -0,0 +1,49 @@
+/****************************************************************************
+**
+** Copyright (C) 2018 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the lottie-qt module of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:GPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 or (at your option) any later version
+** approved by the KDE Free Qt Foundation. The licenses are as published by
+** the Free Software Foundation and appearing in the file LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#include "lottie_plugin.h"
+
+#include <qqml.h>
+
+#include <QtBodymovin/private/bmconstants_p.h>
+
+#include "lottieanimation.h"
+#include "rasterrenderer/batchrenderer.h"
+
+QT_BEGIN_NAMESPACE
+
+void BodymovinPlugin::registerTypes(const char *uri)
+{
+ qmlRegisterType<LottieAnimation>(uri, 1, 0, "LottieAnimation");
+ qmlRegisterType<BMLiteral>(uri, 1, 0, "BMPropertyType");
+
+ BatchRenderer::deleteInstance();
+}
+
+QT_END_NAMESPACE
diff --git a/src/imports/lottie_plugin.h b/src/imports/lottie_plugin.h
new file mode 100644
index 0000000..121f14e
--- /dev/null
+++ b/src/imports/lottie_plugin.h
@@ -0,0 +1,46 @@
+/****************************************************************************
+**
+** Copyright (C) 2018 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the lottie-qt module of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:GPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 or (at your option) any later version
+** approved by the KDE Free Qt Foundation. The licenses are as published by
+** the Free Software Foundation and appearing in the file LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#pragma once
+
+#include <QQmlExtensionPlugin>
+#include <QThread>
+
+QT_BEGIN_NAMESPACE
+
+class BodymovinPlugin : public QQmlExtensionPlugin
+{
+ Q_OBJECT
+ Q_PLUGIN_METADATA(IID QQmlExtensionInterface_iid)
+
+public:
+ void registerTypes(const char *uri);
+};
+
+QT_END_NAMESPACE
diff --git a/src/imports/lottieanimation.cpp b/src/imports/lottieanimation.cpp
new file mode 100644
index 0000000..f8eccd3
--- /dev/null
+++ b/src/imports/lottieanimation.cpp
@@ -0,0 +1,662 @@
+/****************************************************************************
+**
+** Copyright (C) 2018 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the lottie-qt module of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:GPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 or (at your option) any later version
+** approved by the KDE Free Qt Foundation. The licenses are as published by
+** the Free Software Foundation and appearing in the file LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#include "lottieanimation.h"
+
+#include <QQuickPaintedItem>
+#include <QJsonDocument>
+#include <QJsonObject>
+#include <QJsonArray>
+#include <QJsonValue>
+#include <QFile>
+#include <QPointF>
+#include <QPainter>
+#include <QImage>
+#include <QTimer>
+#include <QMetaObject>
+#include <QLoggingCategory>
+#include <QThread>
+#include <math.h>
+
+#include <QtBodymovin/private/bmbase_p.h>
+#include <QtBodymovin/private/bmlayer_p.h>
+
+#include "rasterrenderer/batchrenderer.h"
+#include "rasterrenderer/lottierasterrenderer.h"
+
+QT_BEGIN_NAMESPACE
+
+Q_LOGGING_CATEGORY(lcLottieQtBodymovinRender, "qt.lottieqt.bodymovin.render");
+Q_LOGGING_CATEGORY(lcLottieQtBodymovinParser, "qt.lottieqt.bodymovin.parser");
+
+/*!
+ \qmltype LottieAnimation
+ \instantiates LottieAnimation
+ \inqmlmodule Qt.labs.lottieqt
+ \since 5.13
+ \inherits Item
+ \brief A Bodymovin player for Qt.
+
+ The LottieAnimation type shows Bodymovin format files.
+
+ LottieAnimation is used to load and render Bodymovin files exported
+ from Adobe After Effects. Currently, only subset of the full Bodymovin
+ specification is supported. Most notable deviations are:
+
+ \list
+ \li Only Shape layer supported
+ \li Only integer frame-mode of a timeline supported
+ (real frame numbers and time are rounded to the nearest integer)
+ \li Expressions are not supported
+ \endlist
+
+ For the full list of devations, please refer to the file
+ \c unsupported_features.txt in the source code.
+
+
+ \section1 Example Usage
+
+ The following example shows a simple usage of the LottieAnimation type
+
+ \code
+ LottieAnimation {
+ loops: 2
+ quality: LottieAnimation.MediumQuality
+ source: ":/animation.json"
+ autoPlay: false
+ onStatusChanged: {
+ if (status === LottieAnimation.Ready) {
+ // any acvities needed before
+ // playing starts go here
+ gotoAndPlay(startFrame);
+ }
+ }
+ onFinished: {
+ console.log("Finished playing")
+ }
+ }
+ \endcode
+
+ Note: Changing width or height of the element does not change the size
+ of the animation within. Also, it is not possible to align the the content
+ inside of a \c LottieAnimation element. To achieve this, position the
+ animation inside e.g. an \c Item.
+
+ \section1 Rendering performance
+
+ Internally, the rendered frame data is cached to improve performance. You
+ can control the memory usage by setting the QLOTTIE_RENDER_CACHE_SIZE
+ environment variable (default value is 2).
+
+ You can monitor the rendering performance by turning on two logging categories:
+
+ \list
+ \li \c qt.lottieqt.bodymovin.render - Provides information how the animation
+ is rendered
+ \li \c qt.lottieqt.bodymovin.render.thread - Provides information how the
+ rendering process proceeds.
+ \endlist
+
+ Specifically, you can monitor does the frame cache gets constantly full, or
+ does the rendering process have to wait for frames to become ready. The
+ first case implies that the animation is too complex, and the rendering
+ cannot keep up the pace. Try making the animation simpler, or optimize
+ the QML scene.
+*/
+
+/*!
+ \qmlproperty enumeration LottieAnimation::status
+
+ This property holds the current status of the LottieAnimation element.
+
+ \list
+ \li Null – An initial value that is used when the status is not defined (Default)
+ \li Loading – the player is loading a Bodymovin file
+ \li Ready – loading has finished successfully and the player is ready to play animtion
+ \li Error – an error occurred while the loading
+ \endlist
+
+ For example you could implement \c onStatusChanged signal
+ handler to monitor progress of loading an animation as follows:
+
+ \code
+ LottieAnimation {
+ source: ":/animation.json"
+ autoPlay: false
+ onStatusChanged: {
+ if (status === LottieAnimation.Ready)
+ start();
+ }
+ \endcode
+*/
+
+/*!
+ \qmlproperty bool LottieAnimation::autoPlay
+
+ Defines whether the player will start playing animation automatically after
+ the animation file has been loaded.
+
+ The default value is \c true.
+*/
+
+/*!
+ \qmlproperty int LottieAnimation::loops
+
+ This property holds how many loops the player will repeat.
+ The value \c LottieAnimation.Inifite means, that the the player repeats
+ the animation continuously.
+
+ The default value is \c 1.
+*/
+
+/*!
+ \qmlsignal LottieAnimation::finished()
+
+ Signal is emitted when the player has finished playing. In case of looping,
+ the signal is emitted when the last loop has been finished.
+*/
+
+LottieAnimation::LottieAnimation(QQuickItem *parent)
+ : QQuickPaintedItem(parent)
+{
+ m_frameAdvance = new QTimer(this);
+ m_frameAdvance->setSingleShot(false);
+ connect (m_frameAdvance, &QTimer::timeout, this, &LottieAnimation::renderNextFrame);
+
+ m_frameRenderThread = BatchRenderer::instance();
+
+ QByteArray cacheStr = qgetenv("QLOTTIE_RENDER_CACHE_SIZE");
+ bool ok = false;
+ int cacheSize = cacheStr.toInt(&ok);
+ if (ok)
+ m_frameRenderThread->setCacheSize(cacheSize);
+
+ qRegisterMetaType<LottieAnimation*>();
+}
+
+LottieAnimation::~LottieAnimation()
+{
+ QMetaObject::invokeMethod(m_frameRenderThread, "deregisterAnimator", Q_ARG(LottieAnimation*, this));
+}
+
+void LottieAnimation::componentComplete()
+{
+ QQuickItem::componentComplete();
+
+ m_initialized = true;
+ if (m_source.length())
+ loadSource(m_source);
+}
+
+void LottieAnimation::paint(QPainter *painter)
+{
+ // TODO: Check does this have effect on output quality (or performance)
+ if (m_quality != LowQuality)
+ painter->setRenderHints(QPainter::Antialiasing);
+ if (m_quality == HighQuality)
+ painter->setRenderHints(QPainter::SmoothPixmapTransform);
+
+ LottieRasterRenderer renderer(painter);
+ BMBase* bmTree = m_frameRenderThread->getFrame(this, m_currentFrame);
+
+ if (!bmTree) {
+ qCDebug(lcLottieQtBodymovinRender) << "LottieAnimation::paint: Got empty element tree."
+ "Cannot draw (Animator:" << static_cast<void*>(this) << ")";
+ return;
+ }
+
+ qCDebug(lcLottieQtBodymovinRender) << static_cast<void*>(this) << "Start to paint frame" << m_currentFrame;
+
+ for (BMBase *elem : bmTree->children()) {
+ if (elem->active(m_currentFrame))
+ elem->render(renderer);
+ else
+ qCDebug(lcLottieQtBodymovinRender) << "Element '" << elem->name() << "' inactive. No need to paint";
+ }
+
+ m_frameRenderThread->frameRendered(this, m_currentFrame);
+
+ m_currentFrame += m_direction;
+
+ if (m_currentFrame < m_startFrame || m_currentFrame > m_endFrame) {
+ m_currentLoop += (m_loops > 0 ? 1 : 0);
+ }
+
+ if ((m_loops - m_currentLoop) != 0) {
+ m_currentFrame = m_currentFrame < m_startFrame ? m_endFrame :
+ m_currentFrame > m_endFrame ? m_startFrame : m_currentFrame;
+ }
+}
+
+/*!
+ \qmlproperty string LottieAnimation::source
+
+ The path of the Bodymovin asset that LottieAnimation plays.
+
+ Setting the source property starts loading the animation asynchronously.
+ To monitor progress of loading you can connect to the \c status signal.
+*/
+QString LottieAnimation::source() const
+{
+ return m_source;
+}
+
+void LottieAnimation::setSource(const QString &source)
+{
+ if (m_source != source) {
+ m_source = source;
+ emit sourceChanged();
+
+ if (m_initialized)
+ loadSource(source);
+ }
+}
+
+/*!
+ \qmlproperty int LottieAnimation::startFrame *
+
+ Frame number of the start of the animation. The value
+ is available after the animation has been loaded.
+*/
+int LottieAnimation::startFrame() const
+{
+ return m_startFrame;
+}
+
+/*!
+ \qmlproperty int LottieAnimation::endFrame
+
+ Frame number of the end of the animation. The value
+ is available after the animation has been loaded.
+*/
+int LottieAnimation::endFrame() const
+{
+ return m_endFrame;
+}
+
+int LottieAnimation::currentFrame() const
+{
+ return m_currentFrame;
+}
+
+/*!
+ \qmlproperty int LottieAnimation::frameRate
+
+ This property holds the frame rate value of the Bodymovin animation.
+
+ \c frameRate changes after the asset has been loaded. Changing the
+ frame rate does not have effect before that, as the value defined in the
+ asset overrides the value. To change the frame rate, you can write:
+
+ \code
+ LottieAnimation {
+ source: ":/animation.json"
+ onStatusChanged: {
+ if (status === LottieAnimation.Ready)
+ frameRate = 60;
+ }
+ \endcode
+*/
+int LottieAnimation::frameRate() const
+{
+ return m_frameRate;
+}
+
+void LottieAnimation::setFrameRate(int frameRate)
+{
+ m_frameRate = frameRate;
+ m_frameAdvance->setInterval(1000 / m_frameRate);
+}
+
+/*!
+ \qmlproperty enumeration LottieAnimation::quality
+
+ Speficies the rendering quality of the bodymovin player.
+ If \c LowQuality is selected the rendering will happen into a frame
+ buffer object, whereas with other options, the rendering will be done
+ onto \c QImage (which in turn will be rendered on the screen).
+
+ \list
+ \li LowQuality – Antialiasing or a smooth pixmap transformation algorithm are not used.
+ \li MediumQuality – Antialiasing is used but no smooth pixmap transformation algorithm (Default)
+ \li HighQuality – Antialiasing and a smooth pixmap tranformation algorithm used
+ \endlist
+*/
+LottieAnimation::Quality LottieAnimation::quality() const
+{
+ return m_quality;
+}
+
+void LottieAnimation::setQuality(LottieAnimation::Quality quality)
+{
+ if (m_quality != quality) {
+ m_quality = quality;
+ if (quality == LowQuality)
+ setRenderTarget(QQuickPaintedItem::FramebufferObject);
+ else
+ setRenderTarget(QQuickPaintedItem::Image);
+ emit qualityChanged();
+ }
+}
+
+void LottieAnimation::reset()
+{
+ m_currentFrame = m_direction > 0 ? m_startFrame : m_endFrame;
+ m_currentLoop = 0;
+ QMetaObject::invokeMethod(m_frameRenderThread, "gotoFrame",
+ Q_ARG(LottieAnimation*, this),
+ Q_ARG(int, m_currentFrame));
+}
+
+/*!
+ \qmlmethod void LottieAnimation::start()
+
+ Starts playing the animation from the beginning.
+*/
+void LottieAnimation::start()
+{
+ reset();
+ m_frameAdvance->start();
+}
+
+/*!
+ \qmlmethod void LottieAnimation::play()
+
+ Starts or continues playing from the current position.
+*/
+void LottieAnimation::play()
+{
+ QMetaObject::invokeMethod(m_frameRenderThread, "gotoFrame",
+ Q_ARG(LottieAnimation*, this),
+ Q_ARG(int, m_currentFrame));
+ m_frameAdvance->start();
+}
+
+/*!
+ \qmlmethod void LottieAnimation::pause()
+
+ Pauses playing.
+*/
+void LottieAnimation::pause()
+{
+ m_frameAdvance->stop();
+ QMetaObject::invokeMethod(m_frameRenderThread, "gotoFrame",
+ Q_ARG(LottieAnimation*, this),
+ Q_ARG(int, m_currentFrame));
+}
+
+/*!
+ \qmlmethod void LottieAnimation::togglePause()
+
+ Togglrd the status of player between playing and paused states.
+*/
+void LottieAnimation::togglePause()
+{
+ if (m_frameAdvance->isActive()) {
+ pause();
+ } else {
+ play();
+ }
+}
+
+/*!
+ \qmlmethod void LottieAnimation::stop()
+
+ Stops playing and return to \c startFrame.
+*/
+void LottieAnimation::stop()
+{
+ m_frameAdvance->stop();
+ reset();
+ renderNextFrame();
+}
+
+/*!
+ \qmlmethod void LottieAnimation::gotoAndPlay(int frame)
+
+ Plays the asset from the given \a frame
+*/
+void LottieAnimation::gotoAndPlay(int frame)
+{
+ gotoFrame(frame);
+ m_currentLoop = 0;
+ m_frameAdvance->start();
+}
+
+/*!
+ \qmlmethod bool LottieAnimation::gotoAndPlay(string frameMarker)
+
+ Plays the asset from the frame that has a marker with the given \a frameMarker.
+ Returns true if the frameMarker found in the asset.
+*/
+bool LottieAnimation::gotoAndPlay(const QString &frameMarker)
+{
+ if (m_markers.contains(frameMarker)) {
+ gotoAndPlay(m_markers.value(frameMarker));
+ return true;
+ } else
+ return false;
+}
+
+/*!
+ \qmlmethod void LottieAnimation::gotoAndStop(int frame)
+
+ Moves the playhead to the given frame and stops.
+*/
+void LottieAnimation::gotoAndStop(int frame)
+{
+ gotoFrame(frame);
+ renderNextFrame();
+}
+
+/*!
+ \qmlmethod bool LottieAnimation::gotoAndStop(string frameMarker)
+
+ Moves the playhead to the given marker and stops.
+ Returns true if the frameMarker found in the asset.
+*/
+bool LottieAnimation::gotoAndStop(const QString &frameMarker)
+{
+ if (m_markers.contains(frameMarker)) {
+ gotoAndStop(m_markers.value(frameMarker));
+ return true;
+ } else
+ return false;
+}
+
+void LottieAnimation::gotoFrame(int frame)
+{
+ m_currentFrame = qMax(m_startFrame, qMin(frame, m_endFrame));
+ QMetaObject::invokeMethod(m_frameRenderThread, "gotoFrame",
+ Q_ARG(LottieAnimation*, this),
+ Q_ARG(int, m_currentFrame));
+}
+
+/*!
+ \qmlmethod double LottieAnimation::getDuration(bool inFrames)
+
+ Returns the duration of a currently playing asset.
+ If a given \a inFrames is true, returns value in a number of frames.
+ Otherwise, returns the value in seconds.
+*/
+double LottieAnimation::getDuration(bool inFrames)
+{
+ return (m_endFrame - m_startFrame) /
+ static_cast<double>(inFrames ? 1 : m_frameRate);
+}
+
+/*!
+ \qmlproperty enumeration LottieAnimation::direction
+
+ This property holds the direction of rendering.
+ \list
+ \li Forward
+ \li Reverse
+ \endlist
+
+ The default value is \c Forward.
+*/
+LottieAnimation::Direction LottieAnimation::direction() const
+{
+ if (m_direction < 0)
+ return Reverse;
+ else if (m_direction > 0)
+ return Forward;
+ else {
+ Q_UNREACHABLE();
+ return Forward;
+ }
+}
+
+void LottieAnimation::setDirection(Direction direction)
+{
+ if (direction == Forward) {
+ m_direction = 1;
+ emit directionChanged();
+ } else if (direction == Reverse) {
+ m_direction = -1;
+ emit directionChanged();
+ }
+}
+
+bool LottieAnimation::loadSource(QString filename)
+{
+ QFile sourceFile(filename);
+ if (!sourceFile.open(QIODevice::ReadOnly)) {
+ m_status = Error;
+ emit statusChanged();
+ return false;
+ }
+
+ m_status = Loading;
+ emit statusChanged();
+
+ QByteArray json = sourceFile.readAll();
+ parse(json);
+
+ setWidth(m_animWidth);
+ emit widthChanged();
+ setHeight(m_animHeight);
+ emit heightChanged();
+
+ sourceFile.close();
+
+ QMetaObject::invokeMethod(m_frameRenderThread, "registerAnimator", Q_ARG(LottieAnimation*, this));
+
+ m_frameAdvance->setInterval(1000 / m_frameRate);
+
+ if (m_autoPlay)
+ start();
+
+ m_frameRenderThread->start();
+
+ m_status = Ready;
+ emit statusChanged();
+
+ return true;
+}
+
+QByteArray LottieAnimation::jsonSource() const
+{
+ return m_jsonSource;
+}
+
+void LottieAnimation::renderNextFrame()
+{
+ if (m_currentFrame >= m_startFrame && m_currentFrame <= m_endFrame) {
+ if (m_frameRenderThread->getFrame(this, m_currentFrame)) {
+ update();
+ } else if (!m_waitForFrameConn) {
+ qCDebug(lcLottieQtBodymovinRender) << static_cast<void*>(this)
+ << "Frame cache was empty for frame" << m_currentFrame;
+ m_waitForFrameConn = connect(m_frameRenderThread, &BatchRenderer::frameReady,
+ this, [=](LottieAnimation *target, int frameNumber) {
+ if (target != this)
+ return;
+ qCDebug(lcLottieQtBodymovinRender) << static_cast<void*>(this)
+ << "Frame ready" << frameNumber;
+ disconnect(m_waitForFrameConn);
+ update();
+ });
+ }
+ } else if (m_loops == m_currentLoop) {
+ if ( m_loops != Infinite)
+ m_frameAdvance->stop();
+ emit finished();
+ }
+}
+
+int LottieAnimation::parse(QByteArray jsonSource)
+{
+ m_jsonSource = jsonSource;
+
+ QJsonDocument doc = QJsonDocument::fromJson(jsonSource);
+ QJsonObject rootObj = doc.object();
+
+ if (rootObj.empty()) {
+ m_status = Error;
+ return -1;
+ }
+
+ m_startFrame = rootObj.value(QLatin1String("ip")).toVariant().toInt();
+ m_endFrame = rootObj.value(QLatin1String("op")).toVariant().toInt();
+ m_frameRate = rootObj.value(QLatin1String("fr")).toVariant().toInt();
+ m_animWidth = rootObj.value(QLatin1String("w")).toVariant().toReal();
+ m_animHeight = rootObj.value(QLatin1String("h")).toVariant().toReal();
+
+ setWidth(m_animWidth);
+ setHeight(m_animHeight);
+
+ QJsonArray markerArr = rootObj.value(QLatin1String("markers")).toArray();
+ QJsonArray::const_iterator markerIt = markerArr.constBegin();
+ while (markerIt != markerArr.constEnd()) {
+ QString marker = (*markerIt).toObject().value(QLatin1String("cm")).toString();
+ int frame = (*markerIt).toObject().value(QLatin1String("tm")).toInt();
+ m_markers.insert(marker, frame);
+
+ if ((*markerIt).toObject().value(QLatin1String("dr")).toInt())
+ qCWarning(lcLottieQtBodymovinParser)
+ << "property 'dr' not support in a marker";
+ ++markerIt;
+ }
+
+ if (rootObj.value(QLatin1String("assets")).toArray().count())
+ qCWarning(lcLottieQtBodymovinParser) << "assets not supported";
+
+ if (rootObj.value(QLatin1String("chars")).toArray().count())
+ qCWarning(lcLottieQtBodymovinParser) << "chars not supported";
+
+ emit frameRateChanged();
+ emit startFrameChanged();
+ emit endFrameChanged();
+
+ return 0;
+}
+
+QT_END_NAMESPACE
diff --git a/src/imports/lottieanimation.h b/src/imports/lottieanimation.h
new file mode 100644
index 0000000..a1cc41d
--- /dev/null
+++ b/src/imports/lottieanimation.h
@@ -0,0 +1,163 @@
+/****************************************************************************
+**
+** Copyright (C) 2018 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the lottie-qt module of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:GPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 or (at your option) any later version
+** approved by the KDE Free Qt Foundation. The licenses are as published by
+** the Free Software Foundation and appearing in the file LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#ifndef BMANIMATION_H
+#define BMANIMATION_H
+
+#include <QQuickPaintedItem>
+#include <QByteArray>
+#include <QList>
+#include <QImage>
+#include <QThread>
+#include <QMetaObject>
+
+#include <QtBodymovin/private/bmconstants_p.h>
+
+QT_BEGIN_NAMESPACE
+
+class BMBase;
+class BMLayer;
+class BatchRenderer;
+
+class LottieAnimation : public QQuickPaintedItem
+{
+ Q_OBJECT
+ Q_PROPERTY(QString source READ source WRITE setSource NOTIFY sourceChanged)
+ Q_PROPERTY(int frameRate READ frameRate WRITE setFrameRate NOTIFY frameRateChanged)
+ Q_PROPERTY(int startFrame READ startFrame NOTIFY startFrameChanged)
+ Q_PROPERTY(int endFrame READ endFrame NOTIFY endFrameChanged)
+ Q_PROPERTY(Status status MEMBER m_status NOTIFY statusChanged)
+ Q_PROPERTY(Quality quality READ quality WRITE setQuality NOTIFY qualityChanged)
+ Q_PROPERTY(bool autoPlay MEMBER m_autoPlay NOTIFY autoPlayChanged)
+ Q_PROPERTY(int loops MEMBER m_loops NOTIFY loopsChanged)
+ Q_PROPERTY(Direction direction READ direction WRITE setDirection NOTIFY directionChanged)
+
+public:
+ enum Status{Null, Loading, Ready, Error};
+ Q_ENUM(Status)
+
+ enum Quality{LowQuality, MediumQuality, HighQuality};
+ Q_ENUM(Quality)
+
+ enum Direction{Forward = 1, Reverse};
+ Q_ENUM(Direction)
+
+ enum LoopCount{Infinite = -1};
+ Q_ENUM(LoopCount)
+
+ explicit LottieAnimation(QQuickItem *parent = nullptr);
+ ~LottieAnimation() override;
+
+ void componentComplete() override;
+
+ void paint(QPainter *painter) override;
+
+ QString source() const;
+ void setSource(const QString &source);
+
+ int frameRate() const;
+ void setFrameRate(int frameRate);
+
+ Quality quality() const;
+ void setQuality(Quality quality);
+
+ Direction direction() const;
+ void setDirection(Direction direction);
+
+ int startFrame() const;
+ int endFrame() const;
+ int currentFrame() const;
+
+ Q_INVOKABLE void start();
+
+ Q_INVOKABLE void play();
+ Q_INVOKABLE void pause();
+ Q_INVOKABLE void togglePause();
+ Q_INVOKABLE void stop();
+ Q_INVOKABLE void gotoAndPlay(int frame);
+ Q_INVOKABLE bool gotoAndPlay(const QString &frameMarker);
+ Q_INVOKABLE void gotoAndStop(int frame);
+ Q_INVOKABLE bool gotoAndStop(const QString &frameMarker);
+ Q_INVOKABLE double getDuration(bool inFrames = false);
+
+ QByteArray jsonSource() const;
+
+signals:
+ void statusChanged();
+ void qualityChanged();
+ void sourceChanged();
+ void finished();
+ void frameRateChanged();
+ void autoPlayChanged();
+ void loopsChanged();
+ void directionChanged();
+ void startFrameChanged();
+ void endFrameChanged();
+
+protected slots:
+ void renderNextFrame();
+
+protected:
+ bool loadSource(QString filename);
+
+ virtual int parse(QByteArray jsonSource);
+
+protected:
+ BatchRenderer *m_frameRenderThread = nullptr;
+ QMetaObject::Connection m_waitForFrameConn;
+
+ Status m_status = Null;
+ int m_startFrame = 0;
+ int m_endFrame = 0;
+ int m_frameRate = 30;
+ int m_currentFrame = 0;
+ qreal m_animWidth = 0;
+ qreal m_animHeight = 0;
+ QHash<QString, int> m_markers;
+ QString m_source;
+ QTimer *m_frameAdvance = nullptr;
+
+ void gotoFrame(int frame);
+ void reset();
+
+private:
+ bool m_initialized = false;
+ Quality m_quality = MediumQuality;
+ bool m_autoPlay = true;
+ int m_loops = 1;
+ int m_currentLoop = 0;
+ int m_direction = 1;
+ QByteArray m_jsonSource;
+};
+
+QT_END_NAMESPACE
+
+Q_DECLARE_METATYPE(LottieAnimation*)
+
+#endif // BMANIMATION_H
diff --git a/src/imports/qmldir b/src/imports/qmldir
new file mode 100644
index 0000000..8ad0994
--- /dev/null
+++ b/src/imports/qmldir
@@ -0,0 +1,2 @@
+module Qt.labs.lottieqt
+plugin lottieqt
diff --git a/src/imports/rasterrenderer/batchrenderer.cpp b/src/imports/rasterrenderer/batchrenderer.cpp
new file mode 100644
index 0000000..ca7b00e
--- /dev/null
+++ b/src/imports/rasterrenderer/batchrenderer.cpp
@@ -0,0 +1,296 @@
+/****************************************************************************
+**
+** Copyright (C) 2018 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the lottie-qt module of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:GPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 or (at your option) any later version
+** approved by the KDE Free Qt Foundation. The licenses are as published by
+** the Free Software Foundation and appearing in the file LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#include "batchrenderer.h"
+
+#include <QImage>
+#include <QPainter>
+#include <QHash>
+#include <QMutexLocker>
+#include <QLoggingCategory>
+#include <QThread>
+
+#include <QJsonDocument>
+#include <QJsonArray>
+
+#include <QtBodymovin/private/bmconstants_p.h>
+#include <QtBodymovin/private/bmbase_p.h>
+#include <QtBodymovin/private/bmlayer_p.h>
+
+#include "lottieanimation.h"
+#include "lottierasterrenderer.h"
+
+QT_BEGIN_NAMESPACE
+
+Q_LOGGING_CATEGORY(lcLottieQtBodymovinRenderThread, "qt.lottieqt.bodymovin.render.thread");
+
+BatchRenderer *BatchRenderer::m_rendererInstance = nullptr;
+
+BatchRenderer::~BatchRenderer()
+{
+ qDeleteAll(m_animData);
+ qDeleteAll(m_frameCache);
+}
+
+BatchRenderer *BatchRenderer::instance()
+{
+ if (!m_rendererInstance)
+ m_rendererInstance = new BatchRenderer;
+
+ return m_rendererInstance;
+}
+
+void BatchRenderer::deleteInstance()
+{
+ delete m_rendererInstance;
+ m_rendererInstance = nullptr;
+}
+
+void BatchRenderer::registerAnimator(LottieAnimation *animator)
+{
+ QMutexLocker mlocker(&m_mutex);
+
+ qCDebug(lcLottieQtBodymovinRenderThread) << "Register Animator:"
+ << static_cast<void*>(animator);
+
+ Entry *entry = new Entry;
+ entry->animator = animator;
+ entry->startFrame = animator->startFrame();
+ entry->endFrame = animator->endFrame();
+ entry->currentFrame = animator->startFrame();
+ if (animator->direction() == LottieAnimation::Reverse)
+ entry->animDir = -1;
+ // animDir == 1 by default
+ entry->bmTreeBlueprint = new BMBase;
+ parse(entry->bmTreeBlueprint, animator->jsonSource());
+ m_animData.insert(animator, entry);
+ m_waitCondition.wakeAll();
+}
+
+void BatchRenderer::deregisterAnimator(LottieAnimation *animator)
+{
+ QMutexLocker mlocker(&m_mutex);
+
+ qCDebug(lcLottieQtBodymovinRenderThread) << "Deregister Animator:"
+ << static_cast<void*>(animator);
+
+ Entry *entry = m_animData.value(animator, nullptr);
+ if (entry) {
+ qDeleteAll(entry->frameCache);
+ delete entry->bmTreeBlueprint;
+ delete entry;
+ m_animData.remove(animator);
+ }
+}
+
+bool BatchRenderer::gotoFrame(LottieAnimation *animator, int frame)
+{
+ Entry *entry = m_animData.value(animator, nullptr);
+ if (entry) {
+ QMutexLocker mlocker(&m_mutex);
+ qCDebug(lcLottieQtBodymovinRenderThread) << "Animator:"
+ << static_cast<void*>(animator)
+ << "Goto frame:" << frame;
+ entry->currentFrame = frame;
+ pruneFrameCache(entry);
+ m_waitCondition.wakeAll();
+ return true;
+ }
+ return false;
+}
+
+void BatchRenderer::pruneFrameCache(Entry* e)
+{
+ QHash<int, BMBase*>::iterator it = e->frameCache.begin();
+
+ while (it != e->frameCache.end()) {
+ if (it.key() == e->currentFrame) {
+ ++it;
+ } else {
+ delete it.value();
+ it = e->frameCache.erase(it);
+ }
+ }
+}
+
+BMBase *BatchRenderer::getFrame(LottieAnimation *animator, int frameNumber)
+{
+ QMutexLocker mlocker(&m_mutex);
+
+ Entry *entry = m_animData.value(animator, nullptr);
+ if (entry)
+ return entry->frameCache.value(frameNumber, nullptr);
+ else
+ return nullptr;
+}
+
+void BatchRenderer::setCacheSize(int size)
+{
+ if (size < 1 || isRunning())
+ return;
+
+ qCDebug(lcLottieQtBodymovinRenderThread) << "Setting frame cache size to" << size;
+ m_cacheSize = size;
+}
+
+void BatchRenderer::prerender(Entry *animEntry)
+{
+ LottieAnimation *animator = animEntry->animator;
+
+ if (animEntry->frameCache.count() == m_cacheSize) {
+ qCDebug(lcLottieQtBodymovinRenderThread) << "Animator:" << static_cast<void*>(animEntry->animator)
+ << "Cache full, cannot render more";
+ return;
+ }
+
+ while (animEntry->frameCache.count() < m_cacheSize) {
+ // It may be that the animator has deregistered itself while
+ // te mutex was locked. In that case we cannot render here anymore
+ if (!animEntry->bmTreeBlueprint)
+ break;
+
+ if (!animEntry->frameCache.contains(animEntry->currentFrame)) {
+ BMBase *bmTree = new BMBase(*animEntry->bmTreeBlueprint);
+
+ for (BMBase *elem : bmTree->children()) {
+ if (elem->active(animEntry->currentFrame))
+ elem->updateProperties( animEntry->currentFrame);
+ }
+
+ animEntry->frameCache.insert( animEntry->currentFrame, bmTree);
+ }
+
+ qCDebug(lcLottieQtBodymovinRenderThread) << "Animator:"
+ << static_cast<void*>(animEntry->animator)
+ << "Frame drawn to cache. FN:"
+ << animEntry->currentFrame;
+ emit frameReady(animator, animEntry->currentFrame);
+
+ animEntry->currentFrame += animEntry->animDir;
+
+ if (animEntry->currentFrame > animEntry->endFrame) {
+ animEntry->currentFrame = animEntry->startFrame;
+ } else if (animEntry->currentFrame < animEntry->startFrame) {
+ animEntry->currentFrame = animEntry->endFrame;
+ }
+ }
+}
+
+void BatchRenderer::prerender()
+{
+ QMutexLocker mlocker(&m_mutex);
+ bool wait = true;
+
+ foreach (Entry *e, m_animData) {
+ if (e->frameCache.size() < m_cacheSize) {
+ wait = false;
+ break;
+ }
+ }
+
+ if (wait)
+ m_waitCondition.wait(&m_mutex);
+
+ QHash<LottieAnimation*, Entry*>::iterator it = m_animData.begin();
+ while (it != m_animData.end()) {
+ Entry *e = *it;
+ if (e && e->frameCache.size() < m_cacheSize)
+ prerender(e);
+ ++it;
+ }
+}
+
+void BatchRenderer::frameRendered(LottieAnimation *animator, int frameNumber)
+{
+ Entry *entry = m_animData.value(animator, nullptr);
+ if (entry) {
+ qCDebug(lcLottieQtBodymovinRenderThread) << "Animator:" << static_cast<void*>(animator)
+ << "Remove frame from cache" << frameNumber;
+
+ QMutexLocker mlocker(&m_mutex);
+ BMBase *root = entry->frameCache.value(frameNumber, nullptr);
+ delete root;
+ entry->frameCache.remove(frameNumber);
+ m_waitCondition.wakeAll();
+ }
+}
+
+void BatchRenderer::run()
+{
+ qCDebug(lcLottieQtBodymovinRenderThread) << "rendering thread" << QThread::currentThread();
+
+ while (-1) {
+ if (QThread::currentThread()->isInterruptionRequested())
+ return;
+
+ prerender();
+ }
+}
+
+int BatchRenderer::parse(BMBase* rootElement, QByteArray jsonSource)
+{
+ QJsonDocument doc = QJsonDocument::fromJson(jsonSource);
+ QJsonObject rootObj = doc.object();
+
+ if (rootObj.empty())
+ return -1;
+
+ QJsonArray jsonLayers = rootObj.value(QLatin1String("layers")).toArray();
+ QJsonArray::const_iterator jsonLayerIt = jsonLayers.constEnd();
+ while (jsonLayerIt != jsonLayers.constBegin()) {
+ jsonLayerIt--;
+ QJsonObject jsonLayer = (*jsonLayerIt).toObject();
+ BMLayer *layer = BMLayer::construct(jsonLayer);
+ if (layer) {
+ layer->setParent(rootElement);
+ rootElement->addChild(layer);
+ }
+ }
+
+ // Mask layers must be rendered before the layers they affect to
+ // although they appear before in layer hierarchy. For this reason
+ // move a mask after the affected layers, so it will be rendered first
+ QList<BMBase *> &layers = rootElement->children();
+ int moveTo = -1;
+ for (int i = 0; i < layers.count(); i++) {
+ BMLayer *layer = static_cast<BMLayer*>(layers.at(i));
+ if (layer->isClippedLayer())
+ moveTo = i;
+ if (layer->isMaskLayer()) {
+ qCDebug(lcLottieQtBodymovinParser()) << "Move mask layer"
+ << layers.at(i)->name()
+ << "before" << layers.at(moveTo)->name();
+ layers.move(i, moveTo);
+ }
+ }
+
+ return 0;
+}
+
+QT_END_NAMESPACE
diff --git a/src/imports/rasterrenderer/batchrenderer.h b/src/imports/rasterrenderer/batchrenderer.h
new file mode 100644
index 0000000..b5f0985
--- /dev/null
+++ b/src/imports/rasterrenderer/batchrenderer.h
@@ -0,0 +1,112 @@
+/****************************************************************************
+**
+** Copyright (C) 2018 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the lottie-qt module of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:GPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 or (at your option) any later version
+** approved by the KDE Free Qt Foundation. The licenses are as published by
+** the Free Software Foundation and appearing in the file LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#ifndef BATCHRENDERER_H
+#define BATCHRENDERER_H
+
+#include <QHash>
+#include <QThread>
+#include <QMutex>
+#include <QWaitCondition>
+
+QT_BEGIN_NAMESPACE
+
+class BMBase;
+class QImage;
+class LottieAnimation;
+
+class BatchRenderer : public QThread
+{
+ Q_OBJECT
+
+ struct Entry
+ {
+ LottieAnimation* animator = nullptr;
+ BMBase *bmTreeBlueprint = nullptr;
+ int startFrame = 0;
+ int endFrame = 0;
+ int currentFrame = 0;
+ int animDir = 1;
+ QHash<int, BMBase*> frameCache;
+ };
+
+public:
+ virtual ~BatchRenderer();
+
+ BatchRenderer(BatchRenderer const &) = delete;
+ void operator=(BatchRenderer const&) = delete;
+
+ static BatchRenderer *instance();
+ static void deleteInstance();
+
+ BMBase *getFrame(LottieAnimation *animator, int frameNumber);
+
+signals:
+ void frameReady(LottieAnimation *animator, int frameNumber);
+
+public slots:
+ void registerAnimator(LottieAnimation *animator);
+ void deregisterAnimator(LottieAnimation *animator);
+
+ bool gotoFrame(LottieAnimation *animator, int frame);
+
+ void frameRendered(LottieAnimation *animator, int frameNumber);
+ void setCacheSize(int size);
+
+protected:
+ virtual void run();
+
+ int parse(BMBase* rootElement, QByteArray jsonSource);
+
+ void prerender();
+ void prerender(Entry *animEntry);
+
+protected:
+ QHash<LottieAnimation*, Entry*> m_animData;
+ int m_cacheSize = 2;
+ int m_currentFrame = 0;
+
+ LottieAnimation *m_animation = nullptr;
+ QHash<int, QImage*> m_frameCache;
+
+private:
+ BatchRenderer() = default;
+
+ void pruneFrameCache(Entry* e);
+
+private:
+ static BatchRenderer *m_rendererInstance;
+
+ QMutex m_mutex;
+ QWaitCondition m_waitCondition;
+};
+
+QT_END_NAMESPACE
+
+#endif // BATCHRENDERER_H
diff --git a/src/imports/rasterrenderer/lottierasterrenderer.cpp b/src/imports/rasterrenderer/lottierasterrenderer.cpp
new file mode 100644
index 0000000..0d7ff3d
--- /dev/null
+++ b/src/imports/rasterrenderer/lottierasterrenderer.cpp
@@ -0,0 +1,379 @@
+/****************************************************************************
+**
+** Copyright (C) 2018 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the lottie-qt module of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:GPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 or (at your option) any later version
+** approved by the KDE Free Qt Foundation. The licenses are as published by
+** the Free Software Foundation and appearing in the file LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#include "lottierasterrenderer.h"
+
+#include <QPainter>
+#include <QRectF>
+#include <QBrush>
+#include <QTransform>
+#include <QGradient>
+#include <QPointer>
+#include <QVectorIterator>
+
+#include <QtBodymovin/private/bmshape_p.h>
+#include <QtBodymovin/private/bmfill_p.h>
+#include <QtBodymovin/private/bmgfill_p.h>
+#include <QtBodymovin/private/bmbasictransform_p.h>
+#include <QtBodymovin/private/bmshapetransform_p.h>
+#include <QtBodymovin/private/bmrect_p.h>
+#include <QtBodymovin/private/bmellipse_p.h>
+#include <QtBodymovin/private/bmround_p.h>
+#include <QtBodymovin/private/bmfreeformshape_p.h>
+#include <QtBodymovin/private/bmtrimpath_p.h>
+#include <QtBodymovin/private/bmfilleffect_p.h>
+#include <QtBodymovin/private/bmrepeater_p.h>
+
+QT_BEGIN_NAMESPACE
+
+LottieRasterRenderer::LottieRasterRenderer(QPainter *painter)
+ : m_painter(painter)
+{
+ m_painter->setPen(QPen(Qt::NoPen));
+}
+
+void LottieRasterRenderer::saveState()
+{
+ qCDebug(lcLottieQtBodymovinRender) << "Save painter state";
+ m_painter->save();
+ saveTrimmingState();
+ m_pathStack.push_back(m_unitedPath);
+ m_fillEffectStack.push_back(m_fillEffect);
+ m_unitedPath = QPainterPath();
+}
+
+void LottieRasterRenderer::restoreState()
+{
+ qCDebug(lcLottieQtBodymovinRender) << "Restore painter state";
+ m_painter->restore();
+ restoreTrimmingState();
+ m_unitedPath = m_pathStack.pop();
+ m_fillEffect = m_fillEffectStack.pop();
+}
+
+void LottieRasterRenderer::render(const BMLayer &layer)
+{
+ qCDebug(lcLottieQtBodymovinRender) << "Layer:" << layer.name()
+ << "clip layer" << layer.isClippedLayer();
+
+ if (layer.isMaskLayer())
+ m_buildingClipRegion = true;
+ else if (!m_clipPath.isEmpty()) {
+ if (layer.clipMode() == BMLayer::Alpha)
+ m_painter->setClipPath(m_clipPath);
+ else if (layer.clipMode() == BMLayer::InvertedAlpha) {
+ QPainterPath screen;
+ screen.addRect(0, 0, m_painter->device()->width(),
+ m_painter->device()->height());
+ m_painter->setClipPath(screen - m_clipPath);
+ }
+ else {
+ // Clipping is not applied to paths that have
+ // not setting clipping parameters
+ m_painter->setClipPath(QPainterPath());
+ }
+ m_buildingClipRegion = false;
+ m_clipPath = QPainterPath();
+ }
+}
+
+void LottieRasterRenderer::render(const BMRect &rect)
+{
+ m_painter->save();
+
+ for (int i = 0; i < m_repeatCount; i++) {
+ qCDebug(lcLottieQtBodymovinRender) << rect.name()
+ << rect.position() << rect.size();
+ applyRepeaterTransform(i);
+ if (trimmingState() == LottieRenderer::Individual) {
+ QTransform t = m_painter->transform();
+ QPainterPath tp = t.map(rect.path());
+ tp.addPath(m_unitedPath);
+ m_unitedPath = tp;
+ } else if (m_buildingClipRegion) {
+ QTransform t = m_painter->transform();
+ QPainterPath tp = t.map(rect.path());
+ tp.addPath(m_clipPath);
+ m_clipPath = tp;
+ } else
+ m_painter->drawPath(rect.path());
+ }
+
+ m_painter->restore();
+}
+
+void LottieRasterRenderer::render(const BMEllipse &ellipse)
+{
+ m_painter->save();
+
+ for (int i = 0; i < m_repeatCount; i++) {
+ qCDebug(lcLottieQtBodymovinRender) << "Ellipse:" << ellipse.name()
+ << ellipse.position()
+ << ellipse.size();
+
+ applyRepeaterTransform(i);
+ if (trimmingState() == LottieRenderer::Individual) {
+ QTransform t = m_painter->transform();
+ QPainterPath tp = t.map(ellipse.path());
+ tp.addPath(m_unitedPath);
+ m_unitedPath = tp;
+ } else if (m_buildingClipRegion) {
+ QTransform t = m_painter->transform();
+ QPainterPath tp = t.map(ellipse.path());
+ tp.addPath(m_clipPath);
+ m_clipPath = tp;
+ } else
+ m_painter->drawPath(ellipse.path());
+ }
+
+ m_painter->restore();
+}
+
+void LottieRasterRenderer::render(const BMRound &round)
+{
+ m_painter->save();
+
+ for (int i = 0; i < m_repeatCount; i++) {
+ qCDebug(lcLottieQtBodymovinRender) << "Round:" << round.name()
+ << round.position() << round.radius();
+
+ if (trimmingState() == LottieRenderer::Individual) {
+ QTransform t = m_painter->transform();
+ QPainterPath tp = t.map(round.path());
+ tp.addPath(m_unitedPath);
+ m_unitedPath = tp;
+ } else if (m_buildingClipRegion) {
+ QTransform t = m_painter->transform();
+ QPainterPath tp = t.map(round.path());
+ tp.addPath(m_clipPath);
+ m_clipPath = tp;
+ } else
+ m_painter->drawPath(round.path());
+ }
+
+ m_painter->restore();
+}
+
+void LottieRasterRenderer::render(const BMFill &fill)
+{
+ qCDebug(lcLottieQtBodymovinRender) << "Fill:" <<fill.name()
+ << fill.color();
+
+ if (m_fillEffect)
+ return;
+
+ m_painter->setBrush(fill.color());
+}
+
+void LottieRasterRenderer::render(const BMGFill &gradient)
+{
+ qCDebug(lcLottieQtBodymovinRender) << "Gradient:" << gradient.name()
+ << gradient.value();
+
+ if (m_fillEffect)
+ return;
+
+ if (gradient.value())
+ m_painter->setBrush(*gradient.value());
+ else
+ qCWarning(lcLottieQtBodymovinRender) << "Gradient:"
+ << gradient.name()
+ << "Cannot draw gradient fill";
+}
+
+void LottieRasterRenderer::render(const BMStroke &stroke)
+{
+ qCDebug(lcLottieQtBodymovinRender) << "Stroke:" << stroke.name()
+ << stroke.pen() << stroke.pen().miterLimit();
+
+ if (m_fillEffect)
+ return;
+
+ m_painter->setPen(stroke.pen());
+}
+
+void applyBMTransform(QTransform *xf, const BMBasicTransform &bmxf, bool isBMShapeTransform = false)
+{
+ QPointF pos = bmxf.position();
+ qreal rot = bmxf.rotation();
+ QPointF sca = bmxf.scale();
+ QPointF anc = bmxf.anchorPoint();
+
+ xf->translate(pos.x(), pos.y());
+
+ if (!qFuzzyIsNull(rot))
+ xf->rotate(rot);
+
+ if (isBMShapeTransform) {
+ const BMShapeTransform &shxf = static_cast<const BMShapeTransform &>(bmxf);
+ if (!qFuzzyIsNull(shxf.skew())) {
+ QTransform t(shxf.shearX(), shxf.shearY(), 0, -shxf.shearY(), shxf.shearX(), 0, 0, 0, 1);
+ t *= QTransform(1, 0, 0, shxf.shearAngle(), 1, 0, 0, 0, 1);
+ t *= QTransform(shxf.shearX(), -shxf.shearY(), 0, shxf.shearY(), shxf.shearX(), 0, 0, 0, 1);
+ *xf = t * (*xf);
+ }
+ }
+
+ xf->scale(sca.x(), sca.y());
+ xf->translate(-anc.x(), -anc.y());
+}
+
+void LottieRasterRenderer::render(const BMBasicTransform &transform)
+{
+ QTransform t = m_painter->transform();
+ applyBMTransform(&t, transform);
+ m_painter->setTransform(t);
+ m_painter->setOpacity(m_painter->opacity() * transform.opacity());
+
+ qCDebug(lcLottieQtBodymovinRender) << transform.name()
+ << m_painter->transform()
+ << "opacity:" << m_painter->opacity();
+}
+
+void LottieRasterRenderer::render(const BMShapeTransform &transform)
+{
+ qCDebug(lcLottieQtBodymovinRender) << "Shape transform:" << transform.name()
+ << "of" << transform.parent()->name();
+
+ QTransform t = m_painter->transform();
+ applyBMTransform(&t, transform, true);
+ m_painter->setTransform(t);
+ m_painter->setOpacity(m_painter->opacity() * transform.opacity());
+
+ qCDebug(lcLottieQtBodymovinRender) << transform.name()
+ << m_painter->transform()
+ << m_painter->opacity();
+}
+
+void LottieRasterRenderer::render(const BMFreeFormShape &shape)
+{
+ m_painter->save();
+
+ for (int i = 0; i < m_repeatCount; i ++) {
+ qCDebug(lcLottieQtBodymovinRender) << "Render shape:"
+ << shape.name() << "of"
+ << shape.parent()->name();
+ applyRepeaterTransform(i);
+ if (trimmingState() == LottieRenderer::Individual) {
+ QTransform t = m_painter->transform();
+ QPainterPath tp = t.map(shape.path());
+ tp.addPath(m_unitedPath);
+ m_unitedPath = tp;
+ } else if (m_buildingClipRegion) {
+ QTransform t = m_painter->transform();
+ QPainterPath tp = t.map(shape.path());
+ tp.addPath(m_clipPath);
+ m_clipPath = tp;
+ } else
+ m_painter->drawPath(shape.path());
+ }
+
+ m_painter->restore();
+}
+
+void LottieRasterRenderer::render(const BMTrimPath &trimPath)
+{
+ // TODO: Remove "Individual" trimming to the prerendering thread
+ // Now it is done in the GUI thread
+
+ m_painter->save();
+
+ for (int i = 0; i < m_repeatCount; i ++) {
+ qCDebug(lcLottieQtBodymovinRender) << "Render shape:"
+ << trimPath.name() << "of"
+ << trimPath.parent()->name();
+ applyRepeaterTransform(i);
+ if (!trimPath.simultaneous() && !qFuzzyCompare(0.0, m_unitedPath.length())) {
+ qCDebug(lcLottieQtBodymovinRender) << "Render trim path in the GUI thread";
+ QPainterPath tr = trimPath.trim(m_unitedPath);
+ // Do not use the applied transform, as the transform
+ // is already included in m_unitedPath
+ m_painter->setTransform(QTransform());
+ m_painter->drawPath(tr);
+ }
+ }
+
+ m_painter->restore();
+}
+
+void LottieRasterRenderer::render(const BMFillEffect &effect)
+{
+ qCDebug(lcLottieQtBodymovinRender) << "Fill:" <<effect.name()
+ << effect.color();
+
+ m_fillEffect = &effect;
+ m_painter->setBrush(m_fillEffect->color());
+ m_painter->setOpacity(m_painter->opacity() * m_fillEffect->opacity());
+}
+
+void LottieRasterRenderer::render(const BMRepeater &repeater)
+{
+ qCDebug(lcLottieQtBodymovinRender) << "Repeater:" <<repeater.name()
+ << "count:" << repeater.copies();
+
+ if (m_repeaterTransform) {
+ qCWarning(lcLottieQtBodymovinRender) << "Only one Repeater can be active at a time!";
+ return;
+ }
+
+ m_repeatCount = repeater.copies();
+ m_repeatOffset = repeater.offset();
+
+ // Can store pointer to transform, although the transform
+ // is managed by another thread. The object will be available
+ // until the frame has been rendered
+ m_repeaterTransform = &repeater.transform();
+
+ m_painter->translate(m_repeatOffset * m_repeaterTransform->position().x(),
+ m_repeatOffset * m_repeaterTransform->position().y());
+}
+
+void LottieRasterRenderer::applyRepeaterTransform(int instance)
+{
+ if (!m_repeaterTransform || instance == 0)
+ return;
+
+ QTransform t = m_painter->transform();
+
+ QPointF anchors = -m_repeaterTransform->anchorPoint();
+ QPointF position = m_repeaterTransform->position();
+ QPointF anchoredCenter = anchors + position;
+
+ t.translate(anchoredCenter.x(), anchoredCenter.y());
+ t.rotate(m_repeaterTransform->rotation());
+ t.scale(m_repeaterTransform->scale().x(),
+ m_repeaterTransform->scale().y());
+ m_painter->setTransform(t);
+
+ qreal o =m_repeaterTransform->opacityAtInstance(instance);
+
+ m_painter->setOpacity(m_painter->opacity() * o);
+}
+
+QT_END_NAMESPACE
diff --git a/src/imports/rasterrenderer/lottierasterrenderer.h b/src/imports/rasterrenderer/lottierasterrenderer.h
new file mode 100644
index 0000000..557762a
--- /dev/null
+++ b/src/imports/rasterrenderer/lottierasterrenderer.h
@@ -0,0 +1,88 @@
+/****************************************************************************
+**
+** Copyright (C) 2018 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the lottie-qt module of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:GPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 or (at your option) any later version
+** approved by the KDE Free Qt Foundation. The licenses are as published by
+** the Free Software Foundation and appearing in the file LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#ifndef LOTTIERASTERRENDERER_H
+#define LOTTIERASTERRENDERER_H
+
+#include <QPainterPath>
+#include <QPainter>
+#include <QStack>
+#include <QRegion>
+
+#include <QtBodymovin/private/lottierenderer_p.h>
+
+QT_BEGIN_NAMESPACE
+
+class QPainter;
+
+class LottieRasterRenderer : public LottieRenderer
+{
+public:
+ explicit LottieRasterRenderer(QPainter *m_painter);
+ ~LottieRasterRenderer() override = default;
+
+ void saveState() override;
+ void restoreState() override;
+
+ void render(const BMLayer &layer) override;
+ void render(const BMRect &rect) override;
+ void render(const BMEllipse &ellipse) override;
+ void render(const BMRound &round) override;
+ void render(const BMFill &fill) override;
+ void render(const BMGFill &shape) override;
+ void render(const BMStroke &stroke) override;
+ void render(const BMBasicTransform &transform) override;
+ void render(const BMShapeTransform &transform) override;
+ void render(const BMFreeFormShape &shape) override;
+ void render(const BMTrimPath &trans) override;
+ void render(const BMFillEffect &effect) override;
+ void render(const BMRepeater &repeater) override;
+
+protected:
+ QPainter *m_painter = nullptr;
+ QPainterPath m_unitedPath;
+ // TODO: create a context to handle paths and effect
+ // instead of pushing each to a stack independently
+ QStack<QPainterPath> m_pathStack;
+ QStack<const BMFillEffect*> m_fillEffectStack;
+ const BMFillEffect *m_fillEffect = nullptr;
+ const BMRepeaterTransform *m_repeaterTransform = nullptr;
+ int m_repeatCount = 1;
+ qreal m_repeatOffset = 0.0;
+ bool m_buildingClipRegion = false;
+ QPainterPath m_clipPath;
+
+private:
+ void applyRepeaterTransform(int instance);
+};
+
+QT_END_NAMESPACE
+
+#endif // LOTTIERASTERRENDERER_H
+
diff --git a/src/src.pro b/src/src.pro
new file mode 100644
index 0000000..9a828d3
--- /dev/null
+++ b/src/src.pro
@@ -0,0 +1,5 @@
+TEMPLATE = subdirs
+CONFIG += ordered
+SUBDIRS += \
+ bodymovin \
+ imports
diff --git a/src/unsupported_features.txt b/src/unsupported_features.txt
new file mode 100644
index 0000000..1448804
--- /dev/null
+++ b/src/unsupported_features.txt
@@ -0,0 +1,41 @@
+1. General
+
+- The master specification for bodymovin that is obeyed is at
+ https://github.com/airbnb/lottie-web/tree/master/docs/json
+- Deviations in exported Bodymovin files compared to the specification
+ taken into account as much as possible (when differences noticed)
+- precomps not supported
+- expressions not supported
+- only frame-mode on the timeline supported (no time-mode)
+
+
+2. Animation level
+
+- unsupported properties
+ - assets (reusable text and images)
+ - chars (text)
+
+
+3. Layers
+
+- Only the shape layer supported
+- Only alpha mask adjustment layer supported
+- unsupported properties
+ - ao (auto-orientation)
+ - bm (blend mode)
+ - maskProperties (masks)
+ - sr (time stretch)
+
+
+4. Shapes
+
+- the gstroke (group stroke) element not supported
+- the star element not supported
+- Nested Repeater shapes not supported
+- multiple active trim paths behavior is unpredictable
+ (e.g. trim paths in nested groups)
+
+
+5. Effects
+
+- Only Slide and Layer Fill effects supported
diff --git a/sync.profile b/sync.profile
new file mode 100644
index 0000000..d7005c8
--- /dev/null
+++ b/sync.profile
@@ -0,0 +1,6 @@
+%modules = ( # path to module name map
+ "QtBodymovin" => "$basedir/src/bodymovin",
+);
+
+%moduleheaders = ( # restrict the module headers to those found in relative path
+);
diff --git a/tests/auto/auto.pro b/tests/auto/auto.pro
new file mode 100644
index 0000000..da39e21
--- /dev/null
+++ b/tests/auto/auto.pro
@@ -0,0 +1,2 @@
+TEMPLATE = subdirs
+SUBDIRS += bodymovin
diff --git a/tests/auto/bodymovin/bodymovin.pro b/tests/auto/bodymovin/bodymovin.pro
new file mode 100644
index 0000000..9af152d
--- /dev/null
+++ b/tests/auto/bodymovin/bodymovin.pro
@@ -0,0 +1,2 @@
+TEMPLATE = subdirs
+SUBDIRS += shape
diff --git a/tests/auto/bodymovin/shape/ellipse/ellipse.pro b/tests/auto/bodymovin/shape/ellipse/ellipse.pro
new file mode 100644
index 0000000..fab1bd8
--- /dev/null
+++ b/tests/auto/bodymovin/shape/ellipse/ellipse.pro
@@ -0,0 +1,5 @@
+QT += testlib gui-private bodymovin-private
+CONFIG += c++11
+
+SOURCES = \
+ tst_bmellipse.cpp
diff --git a/tests/auto/bodymovin/shape/ellipse/ellipse_animated_100x80at00to200x40at50100.json b/tests/auto/bodymovin/shape/ellipse/ellipse_animated_100x80at00to200x40at50100.json
new file mode 100644
index 0000000..3883853
--- /dev/null
+++ b/tests/auto/bodymovin/shape/ellipse/ellipse_animated_100x80at00to200x40at50100.json
@@ -0,0 +1 @@
+{"v":"5.3.4","fr":60,"ip":0,"op":180,"w":320,"h":320,"nm":"AnimatedRoundShapeLayer","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[160,160,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"d":2,"ty":"el","s":{"a":1,"k":[{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"n":["0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167"],"t":0,"s":[100,80],"e":[200,40]},{"t":179}],"ix":2},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":0,"s":[0,0],"e":[50,100],"to":[8.33333301544189,16.6666660308838],"ti":[-8.33333301544189,-16.6666660308838]},{"t":179}],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false}],"ip":0,"op":180,"st":0,"bm":0}],"markers":[]}
diff --git a/tests/auto/bodymovin/shape/ellipse/ellipse_direction.json b/tests/auto/bodymovin/shape/ellipse/ellipse_direction.json
new file mode 100644
index 0000000..48faacc
--- /dev/null
+++ b/tests/auto/bodymovin/shape/ellipse/ellipse_direction.json
@@ -0,0 +1 @@
+{"v":"5.3.4","fr":60,"ip":0,"op":180,"w":320,"h":320,"nm":"StaticEllipseShapeLayer","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[160,160,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"d":3,"ty":"el","s":{"a":0,"k":[100,80],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":true}],"ip":0,"op":180,"st":0,"bm":0}],"markers":[]}
diff --git a/tests/auto/bodymovin/shape/ellipse/ellipse_hidden.json b/tests/auto/bodymovin/shape/ellipse/ellipse_hidden.json
new file mode 100644
index 0000000..9463cb6
--- /dev/null
+++ b/tests/auto/bodymovin/shape/ellipse/ellipse_hidden.json
@@ -0,0 +1 @@
+{"v":"5.3.4","fr":60,"ip":0,"op":180,"w":320,"h":320,"nm":"StaticEllipseShapeLayer","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[160,160,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"d":2,"ty":"el","s":{"a":0,"k":[100,80],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":true}],"ip":0,"op":180,"st":0,"bm":0}],"markers":[]}
diff --git a/tests/auto/bodymovin/shape/ellipse/ellipse_static_100x80.json b/tests/auto/bodymovin/shape/ellipse/ellipse_static_100x80.json
new file mode 100644
index 0000000..9158d27
--- /dev/null
+++ b/tests/auto/bodymovin/shape/ellipse/ellipse_static_100x80.json
@@ -0,0 +1 @@
+{"v":"5.3.4","fr":60,"ip":0,"op":180,"w":320,"h":320,"nm":"StaticRoundShapeLayer","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[150,150,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"d":1,"ty":"el","s":{"a":0,"k":[100,80],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false}],"ip":0,"op":180,"st":0,"bm":0}],"markers":[]}
diff --git a/tests/auto/bodymovin/shape/ellipse/tst_bmellipse.cpp b/tests/auto/bodymovin/shape/ellipse/tst_bmellipse.cpp
new file mode 100644
index 0000000..c8dd644
--- /dev/null
+++ b/tests/auto/bodymovin/shape/ellipse/tst_bmellipse.cpp
@@ -0,0 +1,315 @@
+/****************************************************************************
+**
+** Copyright (C) 2018 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the lottie-qt module of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:GPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 or (at your option) any later version
+** approved by the KDE Free Qt Foundation. The licenses are as published by
+** the Free Software Foundation and appearing in the file LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#include <QtTest/QtTest>
+
+#include <QFile>
+#include <QJsonDocument>
+#include <QJsonObject>
+#include <QJsonArray>
+
+#include "private/bmlayer_p.h"
+#include "private/bmellipse_p.h"
+
+class tst_BMEllipse: public QObject
+{
+ Q_OBJECT
+
+public:
+ tst_BMEllipse();
+ ~tst_BMEllipse();
+
+private:
+
+private slots:
+ void initTestCase();
+ void cleanupTestCase();
+
+ void testStaticInitialX();
+ void testStaticInitialY();
+ void testStaticInitialWidth();
+ void testStaticInitialHeight();
+ void testStaticUpdatedX();
+ void testStaticUpdatedY();
+ void testStaticUpdatedWidth();
+ void testStaticUpdatedHeight();
+
+ void testAnimatedInitialX();
+ void testAnimatedInitialY();
+ void testAnimatedInitialWidth();
+ void testAnimatedInitialHeight();
+ void testAnimatedUpdatedX();
+ void testAnimatedUpdatedY();
+ void testAnimatedUpdatedWidth();
+ void testAnimatedUpdatedHeight();
+
+ void testName();
+ void testType();
+ void testActive();
+ void testHidden();
+ void testDirection();
+
+private:
+ void loadTestData(const QByteArray &filename);
+ void updateProperty(int frame);
+
+ BMEllipse *m_ellipse = nullptr;
+};
+
+tst_BMEllipse::tst_BMEllipse()
+{
+
+}
+
+tst_BMEllipse::~tst_BMEllipse()
+{
+
+}
+
+void tst_BMEllipse::initTestCase()
+{
+}
+
+void tst_BMEllipse::cleanupTestCase()
+{
+ if (m_ellipse)
+ delete m_ellipse;
+}
+
+void tst_BMEllipse::testStaticInitialX()
+{
+ loadTestData("ellipse_static_100x80.json");
+
+ QVERIFY(qFuzzyCompare(m_ellipse->position().x(), 0.0));
+}
+
+void tst_BMEllipse:: testStaticInitialY()
+{
+ loadTestData("ellipse_static_100x80.json");
+
+ QVERIFY(qFuzzyCompare(m_ellipse->position().y(), 0.0));
+}
+
+void tst_BMEllipse:: testStaticInitialWidth()
+{
+ loadTestData("ellipse_static_100x80.json");
+
+ QVERIFY(qFuzzyCompare(m_ellipse->size().width(), 100));
+}
+
+void tst_BMEllipse:: testStaticInitialHeight()
+{
+ loadTestData("ellipse_static_100x80.json");
+
+ QVERIFY(qFuzzyCompare(m_ellipse->size().height(), 80));
+}
+
+void tst_BMEllipse::testStaticUpdatedX()
+{
+ loadTestData("ellipse_static_100x80.json");
+ updateProperty(179);
+
+ QVERIFY(qFuzzyCompare(m_ellipse->position().x(), 0.0));
+}
+
+void tst_BMEllipse:: testStaticUpdatedY()
+{
+ loadTestData("ellipse_static_100x80.json");
+ updateProperty(179);
+
+ QVERIFY(qFuzzyCompare(m_ellipse->position().y(), 0.0));
+}
+
+void tst_BMEllipse:: testStaticUpdatedWidth()
+{
+ loadTestData("ellipse_static_100x80.json");
+ updateProperty(179);
+
+ QVERIFY(qFuzzyCompare(m_ellipse->size().width(), 100));
+}
+
+void tst_BMEllipse:: testStaticUpdatedHeight()
+{
+ loadTestData("ellipse_static_100x80.json");
+ updateProperty(179);
+
+ QVERIFY(qFuzzyCompare(m_ellipse->size().height(), 80));
+}
+
+void tst_BMEllipse::testAnimatedInitialX()
+{
+ loadTestData("ellipse_animated_100x80at00to200x40at50100.json");
+ updateProperty(0);
+
+ QVERIFY(qFuzzyCompare(m_ellipse->position().x(), 0.0));
+}
+
+void tst_BMEllipse::testAnimatedInitialY()
+{
+ loadTestData("ellipse_animated_100x80at00to200x40at50100.json");
+ updateProperty(0);
+
+ QVERIFY(qFuzzyCompare(m_ellipse->position().y(), 0.0));
+}
+
+void tst_BMEllipse::testAnimatedInitialWidth()
+{
+ loadTestData("ellipse_animated_100x80at00to200x40at50100.json");
+ updateProperty(0);
+
+ QVERIFY(qFuzzyCompare(m_ellipse->size().width(), 100.0));
+}
+
+void tst_BMEllipse::testAnimatedInitialHeight()
+{
+ loadTestData("ellipse_animated_100x80at00to200x40at50100.json");
+ updateProperty(0);
+
+ QVERIFY(qFuzzyCompare(m_ellipse->size().height(), 80.0));
+}
+
+void tst_BMEllipse::testAnimatedUpdatedX()
+{
+ loadTestData("ellipse_animated_100x80at00to200x40at50100.json");
+ updateProperty(179);
+
+ QVERIFY(qFuzzyCompare(m_ellipse->position().x(), 50.0));
+}
+
+void tst_BMEllipse::testAnimatedUpdatedY()
+{
+ loadTestData("ellipse_animated_100x80at00to200x40at50100.json");
+ updateProperty(179);
+
+ QVERIFY(qFuzzyCompare(m_ellipse->position().y(), 100.0));
+}
+
+void tst_BMEllipse::testAnimatedUpdatedWidth()
+{
+ loadTestData("ellipse_animated_100x80at00to200x40at50100.json");
+ updateProperty(179);
+
+ QVERIFY(qFuzzyCompare(m_ellipse->size().width(), 200.0));
+}
+
+void tst_BMEllipse::testAnimatedUpdatedHeight()
+{
+ loadTestData("ellipse_animated_100x80at00to200x40at50100.json");
+ updateProperty(179);
+
+ QVERIFY(qFuzzyCompare(m_ellipse->size().height(), 40.0));
+}
+
+void tst_BMEllipse::testName()
+{
+ loadTestData("ellipse_static_100x80.json");
+ QVERIFY(m_ellipse->name() == QString("Ellipse Path 1"));
+}
+
+void tst_BMEllipse::testType()
+{
+ loadTestData("ellipse_static_100x80.json");
+ QVERIFY(m_ellipse->type() == BM_SHAPE_ELLIPSE_IX);
+}
+
+void tst_BMEllipse::testActive()
+{
+ loadTestData("ellipse_static_100x80.json");
+ QVERIFY(m_ellipse->active(100) == true);
+
+ loadTestData("ellipse_hidden.json");
+ QVERIFY(m_ellipse->active(100) == false);
+}
+
+void tst_BMEllipse::testHidden()
+{
+ loadTestData("ellipse_hidden.json");
+ QVERIFY(m_ellipse->hidden() == true);
+}
+
+void tst_BMEllipse::testDirection()
+{
+ loadTestData("ellipse_hidden.json");
+ QVERIFY(m_ellipse->direction() == 0);
+ loadTestData("ellipse_direction.json");
+ QVERIFY(m_ellipse->direction() == 0);
+}
+
+void tst_BMEllipse::loadTestData(const QByteArray &filename)
+{
+ if (m_ellipse) {
+ delete m_ellipse;
+ m_ellipse = nullptr;
+ }
+
+ QFile sourceFile(QFINDTESTDATA(filename.constData()));
+ if (!sourceFile.exists())
+ QFAIL("File does not exist");
+ if (!sourceFile.open(QIODevice::ReadOnly))
+ QFAIL("Cannot read test file");
+
+ QByteArray json = sourceFile.readAll();
+
+ sourceFile.close();
+
+ QJsonDocument doc = QJsonDocument::fromJson(json);
+ QJsonObject rootObj = doc.object();
+ if (rootObj.empty())
+ QFAIL("Cannot parse test file");
+
+ QJsonArray layers = rootObj.value(QLatin1String("layers")).toArray();
+ QJsonObject layerObj = layers[0].toObject();
+ int type = layerObj.value(QLatin1String("ty")).toInt();
+ if (type != 4)
+ QFAIL("It's not shape layer");
+
+ QJsonArray shapes = layerObj.value(QLatin1String("shapes")).toArray();
+ QJsonArray::const_iterator shapesIt = shapes.constBegin();
+ BMShape* shape = nullptr;
+ while (shapesIt != shapes.end()) {
+ QJsonObject childObj = (*shapesIt).toObject();
+ shape = BMShape::construct(childObj);
+ QVERIFY(shape != nullptr);
+ if (shape->type() == BM_SHAPE_ELLIPSE_IX)
+ break;
+ shapesIt++;
+ }
+
+ m_ellipse = static_cast<BMEllipse*>(shape);
+
+ QVERIFY(m_ellipse != nullptr);
+}
+
+void tst_BMEllipse::updateProperty(int frame)
+{
+ m_ellipse->updateProperties(frame);
+}
+
+QTEST_MAIN(tst_BMEllipse)
+#include "tst_bmellipse.moc"
diff --git a/tests/auto/bodymovin/shape/fill/.gitignore b/tests/auto/bodymovin/shape/fill/.gitignore
new file mode 100644
index 0000000..fab7372
--- /dev/null
+++ b/tests/auto/bodymovin/shape/fill/.gitignore
@@ -0,0 +1,73 @@
+# This file is used to ignore files which are generated
+# ----------------------------------------------------------------------------
+
+*~
+*.autosave
+*.a
+*.core
+*.moc
+*.o
+*.obj
+*.orig
+*.rej
+*.so
+*.so.*
+*_pch.h.cpp
+*_resource.rc
+*.qm
+.#*
+*.*#
+core
+!core/
+tags
+.DS_Store
+.directory
+*.debug
+Makefile*
+*.prl
+*.app
+moc_*.cpp
+ui_*.h
+qrc_*.cpp
+Thumbs.db
+*.res
+*.rc
+/.qmake.cache
+/.qmake.stash
+
+# qtcreator generated files
+*.pro.user*
+
+# xemacs temporary files
+*.flc
+
+# Vim temporary files
+.*.swp
+
+# Visual Studio generated files
+*.ib_pdb_index
+*.idb
+*.ilk
+*.pdb
+*.sln
+*.suo
+*.vcproj
+*vcproj.*.*.user
+*.ncb
+*.sdf
+*.opensdf
+*.vcxproj
+*vcxproj.*
+
+# MinGW generated files
+*.Debug
+*.Release
+
+# Python byte code
+*.pyc
+
+# Binaries
+# --------
+*.dll
+*.exe
+
diff --git a/tests/auto/bodymovin/shape/fill/fill.pro b/tests/auto/bodymovin/shape/fill/fill.pro
new file mode 100644
index 0000000..f026bb3
--- /dev/null
+++ b/tests/auto/bodymovin/shape/fill/fill.pro
@@ -0,0 +1,5 @@
+QT += testlib gui-private bodymovin-private
+CONFIG += c++11
+
+SOURCES = \
+ tst_bmfill.cpp
diff --git a/tests/auto/bodymovin/shape/fill/fill_animated_red100_green0.json b/tests/auto/bodymovin/shape/fill/fill_animated_red100_green0.json
new file mode 100644
index 0000000..0175b95
--- /dev/null
+++ b/tests/auto/bodymovin/shape/fill/fill_animated_red100_green0.json
@@ -0,0 +1 @@
+{"v":"5.3.4","fr":60,"ip":0,"op":180,"w":320,"h":320,"nm":"AnimatedFillShapeLayer","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[160,160,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"fl","c":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":0,"s":[1,0,0,1],"e":[0,1,0,1]},{"t":179}],"ix":4},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":0,"s":[100],"e":[0]},{"t":179}],"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false}],"ip":0,"op":180,"st":0,"bm":0}],"markers":[]}
diff --git a/tests/auto/bodymovin/shape/fill/fill_hidden.json b/tests/auto/bodymovin/shape/fill/fill_hidden.json
new file mode 100644
index 0000000..ab7c5da
--- /dev/null
+++ b/tests/auto/bodymovin/shape/fill/fill_hidden.json
@@ -0,0 +1 @@
+{"v":"5.3.4","fr":60,"ip":0,"op":180,"w":320,"h":320,"nm":"StaticFillShapeLayer","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[160,160,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"fl","c":{"a":0,"k":[1,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":true}],"ip":0,"op":180,"st":0,"bm":0}],"markers":[]}
diff --git a/tests/auto/bodymovin/shape/fill/fill_static_red_100.json b/tests/auto/bodymovin/shape/fill/fill_static_red_100.json
new file mode 100644
index 0000000..2583b5e
--- /dev/null
+++ b/tests/auto/bodymovin/shape/fill/fill_static_red_100.json
@@ -0,0 +1 @@
+{"v":"5.3.4","fr":60,"ip":0,"op":180,"w":320,"h":320,"nm":"StaticFillShapeLayer","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[160,160,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"fl","c":{"a":0,"k":[1,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false}],"ip":0,"op":180,"st":0,"bm":0}],"markers":[]}
diff --git a/tests/auto/bodymovin/shape/fill/tst_bmfill.cpp b/tests/auto/bodymovin/shape/fill/tst_bmfill.cpp
new file mode 100644
index 0000000..4ea557e
--- /dev/null
+++ b/tests/auto/bodymovin/shape/fill/tst_bmfill.cpp
@@ -0,0 +1,236 @@
+/****************************************************************************
+**
+** Copyright (C) 2018 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the lottie-qt module of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:GPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 or (at your option) any later version
+** approved by the KDE Free Qt Foundation. The licenses are as published by
+** the Free Software Foundation and appearing in the file LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#include <QtTest/QtTest>
+
+#include <QFile>
+#include <QJsonDocument>
+#include <QJsonObject>
+#include <QJsonArray>
+
+#include "private/bmlayer_p.h"
+#include "private/bmfill_p.h"
+
+class tst_BMFill: public QObject
+{
+ Q_OBJECT
+
+public:
+ tst_BMFill();
+ ~tst_BMFill();
+
+private:
+
+private slots:
+ void initTestCase();
+ void cleanupTestCase();
+
+ void testStaticInitialColor();
+ void testStaticeInitialOpacity();
+ void testStaticUpdatedColor();
+ void testStaticeUpdatedOpacity();
+
+ void testAnimatedInitialColor();
+ void testAnimatedInitialOpacity();
+ void testAnimatedUpdatedColor();
+ void testAnimatedUpdatedOpacity();
+
+ void testName();
+ void testType();
+ void testActive();
+ void testHidden();
+
+private:
+ void loadTestData(const QByteArray &filename);
+ void updateProperty(int frame);
+
+ BMFill *m_fill = nullptr;
+};
+
+tst_BMFill::tst_BMFill()
+{
+
+}
+
+tst_BMFill::~tst_BMFill()
+{
+
+}
+
+void tst_BMFill::initTestCase()
+{
+}
+
+void tst_BMFill::cleanupTestCase()
+{
+ if (m_fill)
+ delete m_fill;
+}
+
+void tst_BMFill::testStaticInitialColor()
+{
+ loadTestData("fill_static_red_100.json");
+
+ QVERIFY(m_fill->color() == QColor(Qt::red));
+}
+
+void tst_BMFill:: testStaticeInitialOpacity()
+{
+ loadTestData("fill_static_red_100.json");
+
+ QVERIFY(qFuzzyCompare(m_fill->opacity(), 100.0));
+}
+
+void tst_BMFill::testStaticUpdatedColor()
+{
+ loadTestData("fill_static_red_100.json");
+ updateProperty(179);
+
+ QVERIFY(m_fill->color() == QColor(Qt::red));
+}
+
+void tst_BMFill:: testStaticeUpdatedOpacity()
+{
+ loadTestData("fill_static_red_100.json");
+ updateProperty(179);
+
+ QVERIFY(qFuzzyCompare(m_fill->opacity(), 100.0));
+}
+
+void tst_BMFill::testAnimatedInitialColor()
+{
+ loadTestData("fill_animated_red100_green0.json");
+ updateProperty(0);
+
+ QVERIFY(m_fill->color() == QColor(Qt::red));
+}
+
+void tst_BMFill::testAnimatedInitialOpacity()
+{
+ loadTestData("fill_animated_red100_green0.json");
+ updateProperty(0);
+
+ QVERIFY(qFuzzyCompare(m_fill->opacity(), 100.0));
+}
+
+void tst_BMFill::testAnimatedUpdatedColor()
+{
+ loadTestData("fill_animated_red100_green0.json");
+ updateProperty(179);
+
+ QVERIFY(m_fill->color() == QColor(Qt::green));
+}
+
+void tst_BMFill::testAnimatedUpdatedOpacity()
+{
+ loadTestData("fill_animated_red100_green0.json");
+ updateProperty(179);
+
+ QVERIFY(qFuzzyCompare(m_fill->opacity(), 0.0));
+}
+
+void tst_BMFill::testName()
+{
+ loadTestData("fill_static_red_100.json");
+ QVERIFY(m_fill->name() == QString("Fill 1"));
+}
+
+void tst_BMFill::testType()
+{
+ loadTestData("fill_static_red_100.json");
+ QVERIFY(m_fill->type() == BM_SHAPE_FILL_IX);
+}
+
+void tst_BMFill::testActive()
+{
+ loadTestData("fill_static_red_100.json");
+ QVERIFY(m_fill->active(100) == true);
+
+ loadTestData("fill_hidden.json");
+ QVERIFY(m_fill->active(100) == false);
+}
+
+void tst_BMFill::testHidden()
+{
+ loadTestData("fill_hidden.json");
+ QVERIFY(m_fill->hidden() == true);
+}
+
+void tst_BMFill::loadTestData(const QByteArray &filename)
+{
+ if (m_fill) {
+ delete m_fill;
+ m_fill = nullptr;
+ }
+
+ QFile sourceFile(QFINDTESTDATA(filename.constData()));
+ if (!sourceFile.exists())
+ QFAIL("File does not exist");
+ if (!sourceFile.open(QIODevice::ReadOnly))
+ QFAIL("Cannot read test file");
+
+ QByteArray json = sourceFile.readAll();
+
+ sourceFile.close();
+
+ QJsonDocument doc = QJsonDocument::fromJson(json);
+ QJsonObject rootObj = doc.object();
+ if (rootObj.empty())
+ QFAIL("Cannot parse test file");
+
+ QJsonArray layers = rootObj.value(QLatin1String("layers")).toArray();
+ QJsonObject layerObj = layers[0].toObject();
+ int type = layerObj.value(QLatin1String("ty")).toInt();
+ if (type != 4)
+ QFAIL("It's not shape layer");
+
+ QJsonArray shapes = layerObj.value(QLatin1String("shapes")).toArray();
+ QJsonArray::const_iterator shapesIt = shapes.constBegin();
+ BMShape* shape = nullptr;
+ while (shapesIt != shapes.end()) {
+ QJsonObject childObj = (*shapesIt).toObject();
+ shape = BMShape::construct(childObj);
+ QVERIFY(shape != nullptr);
+ if (shape->type() == BM_SHAPE_FILL_IX)
+ break;
+ shapesIt++;
+ }
+
+ m_fill = static_cast<BMFill*>(shape);
+
+ QVERIFY(m_fill != nullptr);
+}
+
+void tst_BMFill::updateProperty(int frame)
+{
+ m_fill->updateProperties(frame);
+}
+
+QTEST_MAIN(tst_BMFill)
+#include "tst_bmfill.moc"
diff --git a/tests/auto/bodymovin/shape/path/freeform_curve_animated.json b/tests/auto/bodymovin/shape/path/freeform_curve_animated.json
new file mode 100644
index 0000000..619ef39
--- /dev/null
+++ b/tests/auto/bodymovin/shape/path/freeform_curve_animated.json
@@ -0,0 +1 @@
+{"v":"5.3.4","fr":60,"ip":0,"op":180,"w":100,"h":100,"nm":"AnimatedFreeformCurveShapeLayer","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,0,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":0,"s":[{"i":[[0,0],[-17.2,34.503],[5.008,-25.055]],"o":[[0,0],[5.999,-12.033],[-5.149,25.76]],"v":[[15.001,50.066],[45.001,50.072],[85,50.07]],"c":false}],"e":[{"i":[[0,0],[-16.893,-45.111],[-20,-38.07]],"o":[[0,0],[14.999,40.053],[12.217,23.255]],"v":[[15.001,50.066],[45.001,50.072],[85,50.07]],"c":false}]},{"t":179}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false}],"ip":0,"op":180,"st":0,"bm":0}],"markers":[]}
diff --git a/tests/auto/bodymovin/shape/path/freeform_curve_static.json b/tests/auto/bodymovin/shape/path/freeform_curve_static.json
new file mode 100644
index 0000000..d313686
--- /dev/null
+++ b/tests/auto/bodymovin/shape/path/freeform_curve_static.json
@@ -0,0 +1 @@
+{"v":"5.3.4","fr":60,"ip":0,"op":180,"w":100,"h":100,"nm":"StaticFreeformCurveShapeLayer","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,0,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[-17.2,34.503],[5.008,-25.055]],"o":[[0,0],[5.999,-12.033],[-5.149,25.76]],"v":[[15.001,50.066],[45.001,50.072],[85,50.07]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false}],"ip":0,"op":180,"st":0,"bm":0}],"markers":[]}
diff --git a/tests/auto/bodymovin/shape/path/freeform_direction.json b/tests/auto/bodymovin/shape/path/freeform_direction.json
new file mode 100644
index 0000000..f0503c2
--- /dev/null
+++ b/tests/auto/bodymovin/shape/path/freeform_direction.json
@@ -0,0 +1 @@
+{"v":"5.3.4","fr":60,"ip":0,"op":180,"w":100,"h":100,"nm":"StaticFreeformCurveShapeLayer","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,0,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-5.149,25.76],[5.999,-12.033],[0,0]],"o":[[5.008,-25.055],[-17.2,34.503],[0,0]],"v":[[85,50.07],[45.001,50.072],[15.001,50.066]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":true}],"ip":0,"op":180,"st":0,"bm":0}],"markers":[]}
diff --git a/tests/auto/bodymovin/shape/path/freeform_hidden.json b/tests/auto/bodymovin/shape/path/freeform_hidden.json
new file mode 100644
index 0000000..f11376a
--- /dev/null
+++ b/tests/auto/bodymovin/shape/path/freeform_hidden.json
@@ -0,0 +1 @@
+{"v":"5.3.4","fr":60,"ip":0,"op":180,"w":100,"h":100,"nm":"StaticFreeformCurveShapeLayer","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,0,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[-17.2,34.503],[5.008,-25.055]],"o":[[0,0],[5.999,-12.033],[-5.149,25.76]],"v":[[15.001,50.066],[45.001,50.072],[85,50.07]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":true}],"ip":0,"op":180,"st":0,"bm":0}],"markers":[]}
diff --git a/tests/auto/bodymovin/shape/path/freeform_roto_animated.json b/tests/auto/bodymovin/shape/path/freeform_roto_animated.json
new file mode 100644
index 0000000..0aa14ea
--- /dev/null
+++ b/tests/auto/bodymovin/shape/path/freeform_roto_animated.json
@@ -0,0 +1 @@
+{"v":"5.3.4","fr":60,"ip":0,"op":180,"w":100,"h":100,"nm":"AnimatedFreeformRotoShapeLayer","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,0.031,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":0,"s":[{"i":[[-4.727,-4.705],[-4.718,4.714],[4.709,4.687],[4.727,-4.723]],"o":[[4.727,4.705],[4.701,-4.696],[-4.736,-4.714],[-4.718,4.714]],"v":[[40.008,60],[60.016,59.953],[60.007,40.02],[39.96,39.991]],"c":true}],"e":[{"i":[[-3.342,-3.311],[-4.718,4.714],[4.72,4.676],[3.338,-3.335]],"o":[[4.738,4.694],[4.701,-4.696],[-3.352,-3.321],[-3.328,3.325]],"v":[[40.008,60],[60.016,59.953],[60.007,40.02],[49.96,49.991]],"c":true}]},{"t":179}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false}],"ip":0,"op":180,"st":0,"bm":0}],"markers":[]}
diff --git a/tests/auto/bodymovin/shape/path/freeform_roto_static.json b/tests/auto/bodymovin/shape/path/freeform_roto_static.json
new file mode 100644
index 0000000..94d394a
--- /dev/null
+++ b/tests/auto/bodymovin/shape/path/freeform_roto_static.json
@@ -0,0 +1 @@
+{"v":"5.3.4","fr":60,"ip":0,"op":180,"w":100,"h":100,"nm":"StaticFreeformRotoShapeLayer","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,0.031,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-4.727,-4.705],[-4.718,4.714],[4.709,4.687],[4.727,-4.723]],"o":[[4.727,4.705],[4.701,-4.696],[-4.736,-4.714],[-4.718,4.714]],"v":[[40.008,60],[60.016,59.953],[60.007,40.02],[39.96,39.991]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false}],"ip":0,"op":180,"st":0,"bm":0}],"markers":[]}
diff --git a/tests/auto/bodymovin/shape/path/freeform_triangle_animated.json b/tests/auto/bodymovin/shape/path/freeform_triangle_animated.json
new file mode 100644
index 0000000..595eaf6
--- /dev/null
+++ b/tests/auto/bodymovin/shape/path/freeform_triangle_animated.json
@@ -0,0 +1 @@
+{"v":"5.3.4","fr":60,"ip":0,"op":180,"w":100,"h":100,"nm":"AnimatedFreeformTriangleShapeLayer","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,0,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":0,"s":[{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[49.984,30.024],[34.996,50.013],[64.995,49.999]],"c":true}],"e":[{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[49.734,79.274],[34.996,59.951],[64.995,60.124]],"c":true}]},{"t":179}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false}],"ip":0,"op":180,"st":0,"bm":0}],"markers":[]}
diff --git a/tests/auto/bodymovin/shape/path/freeform_triangle_static.json b/tests/auto/bodymovin/shape/path/freeform_triangle_static.json
new file mode 100644
index 0000000..c35c27c
--- /dev/null
+++ b/tests/auto/bodymovin/shape/path/freeform_triangle_static.json
@@ -0,0 +1 @@
+{"v":"5.3.4","fr":60,"ip":0,"op":180,"w":100,"h":100,"nm":"StaticFreeformTriangleShapeLayer","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,0,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[49.984,30.024],[34.996,50.013],[64.995,49.999]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false}],"ip":0,"op":180,"st":0,"bm":0}],"markers":[]}
diff --git a/tests/auto/bodymovin/shape/path/path.pro b/tests/auto/bodymovin/shape/path/path.pro
new file mode 100644
index 0000000..1ce75c6
--- /dev/null
+++ b/tests/auto/bodymovin/shape/path/path.pro
@@ -0,0 +1,5 @@
+QT += testlib gui-private bodymovin-private
+CONFIG += c++11
+
+SOURCES = \
+ tst_bmpath.cpp
diff --git a/tests/auto/bodymovin/shape/path/tst_bmpath.cpp b/tests/auto/bodymovin/shape/path/tst_bmpath.cpp
new file mode 100644
index 0000000..47f6e7c
--- /dev/null
+++ b/tests/auto/bodymovin/shape/path/tst_bmpath.cpp
@@ -0,0 +1,1164 @@
+/****************************************************************************
+**
+** Copyright (C) 2018 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the lottie-qt module of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:GPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 or (at your option) any later version
+** approved by the KDE Free Qt Foundation. The licenses are as published by
+** the Free Software Foundation and appearing in the file LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#include <QtTest/QtTest>
+
+#include <QFile>
+#include <QJsonDocument>
+#include <QJsonObject>
+#include <QJsonArray>
+
+#include "private/bmlayer_p.h"
+#include "private/bmfreeformshape_p.h"
+
+class tst_BMPath: public QObject
+{
+ Q_OBJECT
+
+public:
+ tst_BMPath();
+ ~tst_BMPath();
+
+private:
+
+private slots:
+ void initTestCase();
+ void cleanupTestCase();
+
+ void testStaticCurveInitialStartPoint();
+ void testStaticCurveInitialSecondPoint();
+ void testStaticCurveInitialThirdPoint();
+ void testStaticCurveInitialFourthPoint();
+ void testStaticCurveInitialFifthPoint();
+ void testStaticCurveInitialSixthPoint();
+ void testStaticCurveUpdatedStartPoint();
+ void testStaticCurveUpdatedSecondPoint();
+ void testStaticCurveUpdatedThirdPoint();
+ void testStaticCurveUpdatedFourthPoint();
+ void testStaticCurveUpdatedFifthPoint();
+ void testStaticCurveUpdatedSixthPoint();
+
+ void testStaticTriangleInitialStartPoint();
+ void testStaticTriangleInitialSecondPoint();
+ void testStaticTriangleInitialThirdPoint();
+ void testStaticTriangleInitialClosed();
+ void testStaticTriangleUpdatedStartPoint();
+ void testStaticTriangleUpdatedSecondPoint();
+ void testStaticTriangleUpdatedThirdPoint();
+ void testStaticTriangleUpdatedClosed();
+
+ void testStaticRotoInitialStartPoint();
+ void testStaticRotoInitialSecondPoint();
+ void testStaticRotoInitialThirdPoint();
+ void testStaticRotoInitialFourthPoint();
+ void testStaticRotoInitialFifthPoint();
+ void testStaticRotoInitialSixthPoint();
+ void testStaticRotoInitialSeventhPoint();
+ void testStaticRotoInitialEighthPoint();
+ void testStaticRotoInitialNinthPoint();
+ void testStaticRotoInitialTenthPoint();
+ void testStaticRotoInitialEleventhPoint();
+ void testStaticRotoInitialTwelvthPoint();
+ void testStaticRotoInitialClosed();
+ void testStaticRotoUpdatedStartPoint();
+ void testStaticRotoUpdatedSecondPoint();
+ void testStaticRotoUpdatedThirdPoint();
+ void testStaticRotoUpdatedFourthPoint();
+ void testStaticRotoUpdatedFifthPoint();
+ void testStaticRotoUpdatedSixthPoint();
+ void testStaticRotoUpdatedSeventhPoint();
+ void testStaticRotoUpdatedEighthPoint();
+ void testStaticRotoUpdatedNinthPoint();
+ void testStaticRotoUpdatedTenthPoint();
+ void testStaticRotoUpdatedEleventhPoint();
+ void testStaticRotoUpdatedTwelvthPoint();
+ void testStaticRotoUpdatedClosed();
+
+ void testAnimatedCurveInitialStartPoint();
+ void testAnimatedCurveInitialSecondPoint();
+ void testAnimatedCurveInitialThirdPoint();
+ void testAnimatedCurveInitialFourthPoint();
+ void testAnimatedCurveInitialFifthPoint();
+ void testAnimatedCurveInitialSixthPoint();
+ void testAnimatedCurveUpdatedStartPoint();
+ void testAnimatedCurveUpdatedSecondPoint();
+ void testAnimatedCurveUpdatedThirdPoint();
+ void testAnimatedCurveUpdatedFourthPoint();
+ void testAnimatedCurveUpdatedFifthPoint();
+ void testAnimatedCurveUpdatedSixthPoint();
+
+ void testAnimatedTriangleInitialStartPoint();
+ void testAnimatedTriangleInitialSecondPoint();
+ void testAnimatedTriangleInitialThirdPoint();
+ void testAnimatedTriangleInitialClosed();
+ void testAnimatedTriangleUpdatedStartPoint();
+ void testAnimatedTriangleUpdatedSecondPoint();
+ void testAnimatedTriangleUpdatedThirdPoint();
+ void testAnimatedTriangleUpdatedClosed();
+
+ void testAnimatedRotoInitialStartPoint();
+ void testAnimatedRotoInitialSecondPoint();
+ void testAnimatedRotoInitialThirdPoint();
+ void testAnimatedRotoInitialFourthPoint();
+ void testAnimatedRotoInitialFifthPoint();
+ void testAnimatedRotoInitialSixthPoint();
+ void testAnimatedRotoInitialSeventhPoint();
+ void testAnimatedRotoInitialEighthPoint();
+ void testAnimatedRotoInitialNinthPoint();
+ void testAnimatedRotoInitialTenthPoint();
+ void testAnimatedRotoInitialEleventhPoint();
+ void testAnimatedRotoInitialTwelvthPoint();
+ void testAnimatedRotoInitialClosed();
+ void testAnimatedRotoUpdatedStartPoint();
+ void testAnimatedRotoUpdatedSecondPoint();
+ void testAnimatedRotoUpdatedThirdPoint();
+ void testAnimatedRotoUpdatedFourthPoint();
+ void testAnimatedRotoUpdatedFifthPoint();
+ void testAnimatedRotoUpdatedSixthPoint();
+ void testAnimatedRotoUpdatedSeventhPoint();
+ void testAnimatedRotoUpdatedEighthPoint();
+ void testAnimatedRotoUpdatedNinthPoint();
+ void testAnimatedRotoUpdatedTenthPoint();
+ void testAnimatedRotoUpdatedEleventhPoint();
+ void testAnimatedRotoUpdatedTwelvthPoint();
+ void testAnimatedRotoUpdatedClosed();
+
+ void testName();
+ void testType();
+ void testActive();
+ void testHidden();
+ void testDirection();
+
+private:
+ void loadTestData(const QByteArray &filename);
+ void updateProperty(int frame);
+
+ BMFreeFormShape *m_path = nullptr;
+};
+
+tst_BMPath::tst_BMPath()
+{
+
+}
+
+tst_BMPath::~tst_BMPath()
+{
+
+}
+
+void tst_BMPath::initTestCase()
+{
+}
+
+void tst_BMPath::cleanupTestCase()
+{
+ if (m_path)
+ delete m_path;
+}
+
+void tst_BMPath::testStaticCurveInitialStartPoint()
+{
+ loadTestData("freeform_curve_static.json");
+
+ QPainterPath::Element el = m_path->path().elementAt(0);
+ QVERIFY(qRound(el.x) == 15);
+ QVERIFY(qRound(el.y) == 50);
+}
+
+void tst_BMPath::testStaticCurveInitialSecondPoint()
+{
+ loadTestData("freeform_curve_static.json");
+
+ QPainterPath::Element el = m_path->path().elementAt(2);
+ QVERIFY(qRound(el.x) == 28);
+ QVERIFY(qRound(el.y) == 85);
+}
+
+void tst_BMPath::testStaticCurveInitialThirdPoint()
+{
+ loadTestData("freeform_curve_static.json");
+
+ QPainterPath::Element el = m_path->path().elementAt(3);
+ QVERIFY(qRound(el.x) == 45);
+ QVERIFY(qRound(el.y) == 50);
+}
+
+void tst_BMPath::testStaticCurveInitialFourthPoint()
+{
+ loadTestData("freeform_curve_static.json");
+
+ QPainterPath::Element el = m_path->path().elementAt(4);
+ QVERIFY(qRound(el.x) == 51);
+ QVERIFY(qRound(el.y) == 38);
+}
+
+void tst_BMPath::testStaticCurveInitialFifthPoint()
+{
+ loadTestData("freeform_curve_static.json");
+
+ QPainterPath::Element el = m_path->path().elementAt(5);
+ QVERIFY(qRound(el.x) == 90);
+ QVERIFY(qRound(el.y) == 25);
+}
+
+void tst_BMPath::testStaticCurveInitialSixthPoint()
+{
+ loadTestData("freeform_curve_static.json");
+
+ QPainterPath::Element el = m_path->path().elementAt(6);
+ QVERIFY(qRound(el.x) == 85);
+ QVERIFY(qRound(el.y) == 50);
+}
+
+void tst_BMPath::testStaticCurveUpdatedStartPoint()
+{
+ loadTestData("freeform_curve_static.json");
+
+ QPainterPath::Element el = m_path->path().elementAt(0);
+ QVERIFY(qRound(el.x) == 15);
+ QVERIFY(qRound(el.y) == 50);
+}
+
+void tst_BMPath::testStaticCurveUpdatedSecondPoint()
+{
+ loadTestData("freeform_curve_static.json");
+
+ QPainterPath::Element el = m_path->path().elementAt(2);
+ QVERIFY(qRound(el.x) == 28);
+ QVERIFY(qRound(el.y) == 85);
+}
+
+void tst_BMPath::testStaticCurveUpdatedThirdPoint()
+{
+ loadTestData("freeform_curve_static.json");
+
+ QPainterPath::Element el = m_path->path().elementAt(3);
+ QVERIFY(qRound(el.x) == 45);
+ QVERIFY(qRound(el.y) == 50);
+}
+
+void tst_BMPath::testStaticCurveUpdatedFourthPoint()
+{
+ loadTestData("freeform_curve_static.json");
+
+ QPainterPath::Element el = m_path->path().elementAt(4);
+ QVERIFY(qRound(el.x) == 51);
+ QVERIFY(qRound(el.y) == 38);
+}
+
+void tst_BMPath::testStaticCurveUpdatedFifthPoint()
+{
+ loadTestData("freeform_curve_static.json");
+
+ QPainterPath::Element el = m_path->path().elementAt(5);
+ QVERIFY(qRound(el.x) == 90);
+ QVERIFY(qRound(el.y) == 25);
+}
+
+void tst_BMPath::testStaticCurveUpdatedSixthPoint()
+{
+ loadTestData("freeform_curve_static.json");
+
+ QPainterPath::Element el = m_path->path().elementAt(6);
+ QVERIFY(qRound(el.x) == 85);
+ QVERIFY(qRound(el.y) == 50);
+}
+
+void tst_BMPath::testStaticTriangleInitialStartPoint()
+{
+ loadTestData("freeform_triangle_static.json");
+
+ QPainterPath::Element el = m_path->path().elementAt(0);
+ QVERIFY(qRound(el.x) == 50);
+ QVERIFY(qRound(el.y) == 30);
+}
+
+void tst_BMPath::testStaticTriangleInitialSecondPoint()
+{
+ loadTestData("freeform_triangle_static.json");
+
+ QPainterPath::Element el = m_path->path().elementAt(2);
+ QVERIFY(qRound(el.x) == 35);
+ QVERIFY(qRound(el.y) == 50);
+}
+
+void tst_BMPath::testStaticTriangleInitialThirdPoint()
+{
+ loadTestData("freeform_triangle_static.json");
+
+ QPainterPath::Element el = m_path->path().elementAt(5);
+ QVERIFY(qRound(el.x) == 65);
+ QVERIFY(qRound(el.y) == 50);
+}
+
+void tst_BMPath::testStaticTriangleInitialClosed()
+{
+ loadTestData("freeform_triangle_static.json");
+
+ QPainterPath::Element el1 = m_path->path().elementAt(0);
+ QPainterPath::Element el2 = m_path->path().elementAt(m_path->path().elementCount()-1);
+ QVERIFY(qFuzzyCompare(el1.x, el2.x));
+ QVERIFY(qFuzzyCompare(el1.y, el2.y));
+}
+
+void tst_BMPath::testStaticTriangleUpdatedStartPoint()
+{
+ loadTestData("freeform_triangle_static.json");
+
+ QPainterPath::Element el = m_path->path().elementAt(0);
+ QVERIFY(qRound(el.x) == 50);
+ QVERIFY(qRound(el.y) == 30);
+}
+
+void tst_BMPath::testStaticTriangleUpdatedSecondPoint()
+{
+ loadTestData("freeform_triangle_static.json");
+
+ QPainterPath::Element el = m_path->path().elementAt(2);
+ QVERIFY(qRound(el.x) == 35);
+ QVERIFY(qRound(el.y) == 50);
+}
+
+void tst_BMPath::testStaticTriangleUpdatedThirdPoint()
+{
+ loadTestData("freeform_triangle_static.json");
+ QPainterPath::Element el = m_path->path().elementAt(5);
+ QVERIFY(qRound(el.x) == 65);
+ QVERIFY(qRound(el.y) == 50);
+}
+
+void tst_BMPath::testStaticTriangleUpdatedClosed()
+{
+ loadTestData("freeform_triangle_static.json");
+ QPainterPath::Element el1 = m_path->path().elementAt(0);
+ QPainterPath::Element el2 = m_path->path().elementAt(m_path->path().elementCount()-1);
+ QVERIFY(qFuzzyCompare(el1.x, el2.x));
+ QVERIFY(qFuzzyCompare(el1.y, el2.y));
+}
+
+void tst_BMPath::testStaticRotoInitialStartPoint()
+{
+ loadTestData("freeform_roto_static.json");
+
+ QPainterPath::Element el = m_path->path().elementAt(0);
+ QVERIFY(qRound(el.x) == 40);
+ QVERIFY(qRound(el.y) == 60);
+}
+
+void tst_BMPath::testStaticRotoInitialSecondPoint()
+{
+ loadTestData("freeform_roto_static.json");
+
+ QPainterPath::Element el = m_path->path().elementAt(1);
+ QVERIFY(qRound(el.x) == 45);
+ QVERIFY(qRound(el.y) == 65);
+}
+
+void tst_BMPath::testStaticRotoInitialThirdPoint()
+{
+ loadTestData("freeform_roto_static.json");
+
+ QPainterPath::Element el = m_path->path().elementAt(2);
+ QVERIFY(qRound(el.x) == 55);
+ QVERIFY(qRound(el.y) == 65);
+}
+
+void tst_BMPath::testStaticRotoInitialFourthPoint()
+{
+ loadTestData("freeform_roto_static.json");
+
+ QPainterPath::Element el = m_path->path().elementAt(3);
+ QVERIFY(qRound(el.x) == 60);
+ QVERIFY(qRound(el.y) == 60);
+}
+
+void tst_BMPath::testStaticRotoInitialFifthPoint()
+{
+ loadTestData("freeform_roto_static.json");
+
+ QPainterPath::Element el = m_path->path().elementAt(4);
+ QVERIFY(qRound(el.x) == 65);
+ QVERIFY(qRound(el.y) == 55);
+}
+
+void tst_BMPath::testStaticRotoInitialSixthPoint()
+{
+ loadTestData("freeform_roto_static.json");
+
+ QPainterPath::Element el = m_path->path().elementAt(5);
+ QVERIFY(qRound(el.x) == 65);
+ QVERIFY(qRound(el.y) == 45);
+}
+
+void tst_BMPath::testStaticRotoInitialSeventhPoint()
+{
+ loadTestData("freeform_roto_static.json");
+
+ QPainterPath::Element el = m_path->path().elementAt(6);
+ QVERIFY(qRound(el.x) == 60);
+ QVERIFY(qRound(el.y) == 40);
+}
+
+void tst_BMPath::testStaticRotoInitialEighthPoint()
+{
+ loadTestData("freeform_roto_static.json");
+
+ QPainterPath::Element el = m_path->path().elementAt(7);
+ QVERIFY(qRound(el.x) == 55);
+ QVERIFY(qRound(el.y) == 35);
+}
+
+void tst_BMPath::testStaticRotoInitialNinthPoint()
+{
+ loadTestData("freeform_roto_static.json");
+
+ QPainterPath::Element el = m_path->path().elementAt(8);
+ QVERIFY(qRound(el.x) == 45);
+ QVERIFY(qRound(el.y) == 35);
+}
+
+void tst_BMPath::testStaticRotoInitialTenthPoint()
+{
+ loadTestData("freeform_roto_static.json");
+
+ QPainterPath::Element el = m_path->path().elementAt(9);
+ QVERIFY(qRound(el.x) == 40);
+ QVERIFY(qRound(el.y) == 40);
+}
+
+void tst_BMPath::testStaticRotoInitialEleventhPoint()
+{
+ loadTestData("freeform_roto_static.json");
+
+ QPainterPath::Element el = m_path->path().elementAt(10);
+ QVERIFY(qRound(el.x) == 35);
+ QVERIFY(qRound(el.y) == 45);
+}
+
+void tst_BMPath::testStaticRotoInitialTwelvthPoint()
+{
+ loadTestData("freeform_roto_static.json");
+
+ QPainterPath::Element el = m_path->path().elementAt(11);
+ QVERIFY(qRound(el.x) == 35);
+ QVERIFY(qRound(el.y) == 55);
+}
+
+void tst_BMPath::testStaticRotoInitialClosed()
+{
+ loadTestData("freeform_roto_static.json");
+
+ QPainterPath::Element el1 = m_path->path().elementAt(0);
+ QPainterPath::Element el2 = m_path->path().elementAt(m_path->path().elementCount()-1);
+ QVERIFY(qFuzzyCompare(el1.x, el2.x));
+ QVERIFY(qFuzzyCompare(el1.y, el2.y));
+}
+
+void tst_BMPath::testStaticRotoUpdatedStartPoint()
+{
+ loadTestData("freeform_roto_static.json");
+ updateProperty(179);
+
+ QPainterPath::Element el = m_path->path().elementAt(0);
+ QVERIFY(qRound(el.x) == 40);
+ QVERIFY(qRound(el.y) == 60);
+}
+
+void tst_BMPath::testStaticRotoUpdatedSecondPoint()
+{
+ loadTestData("freeform_roto_static.json");
+ updateProperty(179);
+
+ QPainterPath::Element el = m_path->path().elementAt(1);
+ QVERIFY(qRound(el.x) == 45);
+ QVERIFY(qRound(el.y) == 65);
+}
+
+void tst_BMPath::testStaticRotoUpdatedThirdPoint()
+{
+ loadTestData("freeform_roto_static.json");
+ updateProperty(179);
+
+ QPainterPath::Element el = m_path->path().elementAt(2);
+ QVERIFY(qRound(el.x) == 55);
+ QVERIFY(qRound(el.y) == 65);
+}
+
+void tst_BMPath::testStaticRotoUpdatedFourthPoint()
+{
+ loadTestData("freeform_roto_static.json");
+ updateProperty(179);
+
+ QPainterPath::Element el = m_path->path().elementAt(3);
+ QVERIFY(qRound(el.x) == 60);
+ QVERIFY(qRound(el.y) == 60);
+}
+
+void tst_BMPath::testStaticRotoUpdatedFifthPoint()
+{
+ loadTestData("freeform_roto_static.json");
+ updateProperty(179);
+
+ QPainterPath::Element el = m_path->path().elementAt(4);
+ QVERIFY(qRound(el.x) == 65);
+ QVERIFY(qRound(el.y) == 55);
+}
+
+void tst_BMPath::testStaticRotoUpdatedSixthPoint()
+{
+ loadTestData("freeform_roto_static.json");
+ updateProperty(179);
+
+ QPainterPath::Element el = m_path->path().elementAt(5);
+ QVERIFY(qRound(el.x) == 65);
+ QVERIFY(qRound(el.y) == 45);
+}
+
+void tst_BMPath::testStaticRotoUpdatedSeventhPoint()
+{
+ loadTestData("freeform_roto_static.json");
+ updateProperty(179);
+
+ QPainterPath::Element el = m_path->path().elementAt(6);
+ QVERIFY(qRound(el.x) == 60);
+ QVERIFY(qRound(el.y) == 40);
+}
+
+void tst_BMPath::testStaticRotoUpdatedEighthPoint()
+{
+ loadTestData("freeform_roto_static.json");
+ updateProperty(179);
+
+ QPainterPath::Element el = m_path->path().elementAt(7);
+ QVERIFY(qRound(el.x) == 55);
+ QVERIFY(qRound(el.y) == 35);
+}
+
+void tst_BMPath::testStaticRotoUpdatedNinthPoint()
+{
+ loadTestData("freeform_roto_static.json");
+ updateProperty(179);
+
+ QPainterPath::Element el = m_path->path().elementAt(8);
+ QVERIFY(qRound(el.x) == 45);
+ QVERIFY(qRound(el.y) == 35);
+}
+
+void tst_BMPath::testStaticRotoUpdatedTenthPoint()
+{
+ loadTestData("freeform_roto_static.json");
+ updateProperty(179);
+
+ QPainterPath::Element el = m_path->path().elementAt(9);
+ QVERIFY(qRound(el.x) == 40);
+ QVERIFY(qRound(el.y) == 40);
+}
+
+void tst_BMPath::testStaticRotoUpdatedEleventhPoint()
+{
+ loadTestData("freeform_roto_static.json");
+ updateProperty(179);
+
+ QPainterPath::Element el = m_path->path().elementAt(10);
+ QVERIFY(qRound(el.x) == 35);
+ QVERIFY(qRound(el.y) == 45);
+}
+
+void tst_BMPath::testStaticRotoUpdatedTwelvthPoint()
+{
+ loadTestData("freeform_roto_static.json");
+ updateProperty(179);
+
+ QPainterPath::Element el = m_path->path().elementAt(11);
+ QVERIFY(qRound(el.x) == 35);
+ QVERIFY(qRound(el.y) == 55);
+}
+
+void tst_BMPath::testStaticRotoUpdatedClosed()
+{
+ loadTestData("freeform_roto_static.json");
+ updateProperty(179);
+
+ QPainterPath::Element el1 = m_path->path().elementAt(0);
+ QPainterPath::Element el2 = m_path->path().elementAt(m_path->path().elementCount()-1);
+ QVERIFY(qFuzzyCompare(el1.x, el2.x));
+ QVERIFY(qFuzzyCompare(el1.y, el2.y));
+}
+
+void tst_BMPath::testAnimatedCurveInitialStartPoint()
+{
+ loadTestData("freeform_curve_animated.json");
+ updateProperty(0);
+
+ QPainterPath::Element el = m_path->path().elementAt(0);
+ QVERIFY(qRound(el.x) == 15);
+ QVERIFY(qRound(el.y) == 50);
+}
+
+void tst_BMPath::testAnimatedCurveInitialSecondPoint()
+{
+ loadTestData("freeform_curve_animated.json");
+ updateProperty(0);
+
+ QPainterPath::Element el = m_path->path().elementAt(2);
+ QVERIFY(qRound(el.x) == 28);
+ QVERIFY(qRound(el.y) == 85);
+}
+
+void tst_BMPath::testAnimatedCurveInitialThirdPoint()
+{
+ loadTestData("freeform_curve_animated.json");
+ updateProperty(0);
+
+ QPainterPath::Element el = m_path->path().elementAt(3);
+ QVERIFY(qRound(el.x) == 45);
+ QVERIFY(qRound(el.y) == 50);
+}
+
+void tst_BMPath::testAnimatedCurveInitialFourthPoint()
+{
+ loadTestData("freeform_curve_animated.json");
+ updateProperty(0);
+
+ QPainterPath::Element el = m_path->path().elementAt(4);
+ QVERIFY(qRound(el.x) == 51);
+ QVERIFY(qRound(el.y) == 38);
+}
+
+void tst_BMPath::testAnimatedCurveInitialFifthPoint()
+{
+ loadTestData("freeform_curve_animated.json");
+ updateProperty(0);
+
+ QPainterPath::Element el = m_path->path().elementAt(5);
+ QVERIFY(qRound(el.x) == 90);
+ QVERIFY(qRound(el.y) == 25);
+}
+
+void tst_BMPath::testAnimatedCurveInitialSixthPoint()
+{
+ loadTestData("freeform_curve_animated.json");
+ updateProperty(0);
+
+ QPainterPath::Element el = m_path->path().elementAt(6);
+ QVERIFY(qRound(el.x) == 85);
+ QVERIFY(qRound(el.y) == 50);
+}
+
+void tst_BMPath::testAnimatedCurveUpdatedStartPoint()
+{
+ loadTestData("freeform_curve_animated.json");
+ updateProperty(179);
+
+ QPainterPath::Element el = m_path->path().elementAt(0);
+ QVERIFY(qRound(el.x) == 15);
+ QVERIFY(qRound(el.y) == 50);
+}
+
+void tst_BMPath::testAnimatedCurveUpdatedSecondPoint()
+{
+ loadTestData("freeform_curve_animated.json");
+ updateProperty(179);
+
+ QPainterPath::Element el = m_path->path().elementAt(2);
+ QVERIFY(qRound(el.x) == 28);
+ QVERIFY(qRound(el.y) == 5);
+}
+
+void tst_BMPath::testAnimatedCurveUpdatedThirdPoint()
+{
+ loadTestData("freeform_curve_animated.json");
+ updateProperty(179);
+
+ QPainterPath::Element el = m_path->path().elementAt(3);
+ QVERIFY(qRound(el.x) == 45);
+ QVERIFY(qRound(el.y) == 50);
+}
+
+void tst_BMPath::testAnimatedCurveUpdatedFourthPoint()
+{
+ loadTestData("freeform_curve_animated.json");
+ updateProperty(179);
+
+ QPainterPath::Element el = m_path->path().elementAt(4);
+ QVERIFY(qRound(el.x) == 60);
+ QVERIFY(qRound(el.y) == 90);
+}
+
+void tst_BMPath::testAnimatedCurveUpdatedFifthPoint()
+{
+ loadTestData("freeform_curve_animated.json");
+ updateProperty(179);
+
+ QPainterPath::Element el = m_path->path().elementAt(5);
+ QVERIFY(qRound(el.x) == 65);
+ QVERIFY(qRound(el.y) == 12);
+}
+
+void tst_BMPath::testAnimatedCurveUpdatedSixthPoint()
+{
+ loadTestData("freeform_curve_animated.json");
+ updateProperty(179);
+
+ QPainterPath::Element el = m_path->path().elementAt(6);
+ QVERIFY(qRound(el.x) == 85);
+ QVERIFY(qRound(el.y) == 50);
+}
+
+void tst_BMPath::testAnimatedTriangleInitialStartPoint()
+{
+ loadTestData("freeform_triangle_animated.json");
+ updateProperty(0);
+
+ QPainterPath::Element el = m_path->path().elementAt(0);
+ QVERIFY(qRound(el.x) == 50);
+ QVERIFY(qRound(el.y) == 30);
+}
+
+void tst_BMPath::testAnimatedTriangleInitialSecondPoint()
+{
+ loadTestData("freeform_triangle_animated.json");
+ updateProperty(0);
+
+ QPainterPath::Element el = m_path->path().elementAt(2);
+ QVERIFY(qRound(el.x) == 35);
+ QVERIFY(qRound(el.y) == 50);
+}
+
+void tst_BMPath::testAnimatedTriangleInitialThirdPoint()
+{
+ loadTestData("freeform_triangle_animated.json");
+ updateProperty(0);
+
+ QPainterPath::Element el = m_path->path().elementAt(5);
+ QVERIFY(qRound(el.x) == 65);
+ QVERIFY(qRound(el.y) == 50);
+}
+
+void tst_BMPath::testAnimatedTriangleInitialClosed()
+{
+ loadTestData("freeform_triangle_animated.json");
+ updateProperty(0);
+
+ QPainterPath::Element el1 = m_path->path().elementAt(0);
+ QPainterPath::Element el2 = m_path->path().elementAt(m_path->path().elementCount()-1);
+ QVERIFY(qFuzzyCompare(el1.x, el2.x));
+ QVERIFY(qFuzzyCompare(el1.y, el2.y));
+}
+
+void tst_BMPath::testAnimatedTriangleUpdatedStartPoint()
+{
+ loadTestData("freeform_triangle_animated.json");
+ updateProperty(179);
+
+ QPainterPath::Element el = m_path->path().elementAt(0);
+ QVERIFY(qRound(el.x) == 50);
+ QVERIFY(qRound(el.y) == 79);
+}
+
+void tst_BMPath::testAnimatedTriangleUpdatedSecondPoint()
+{
+ loadTestData("freeform_triangle_animated.json");
+ updateProperty(179);
+
+ QPainterPath::Element el = m_path->path().elementAt(2);
+ QVERIFY(qRound(el.x) == 35);
+ QVERIFY(qRound(el.y) == 60);
+}
+
+void tst_BMPath::testAnimatedTriangleUpdatedThirdPoint()
+{
+ loadTestData("freeform_triangle_animated.json");
+ updateProperty(179);
+
+ QPainterPath::Element el = m_path->path().elementAt(5);
+ QVERIFY(qRound(el.x) == 65);
+ QVERIFY(qRound(el.y) == 60);
+}
+
+void tst_BMPath::testAnimatedTriangleUpdatedClosed()
+{
+ loadTestData("freeform_triangle_animated.json");
+ updateProperty(179);
+
+ QPainterPath::Element el1 = m_path->path().elementAt(0);
+ QPainterPath::Element el2 = m_path->path().elementAt(m_path->path().elementCount()-1);
+ QVERIFY(qFuzzyCompare(el1.x, el2.x));
+ QVERIFY(qFuzzyCompare(el1.y, el2.y));
+}
+
+void tst_BMPath::testAnimatedRotoInitialStartPoint()
+{
+ loadTestData("freeform_roto_animated.json");
+ updateProperty(0);
+
+ QPainterPath::Element el = m_path->path().elementAt(0);
+ QVERIFY(qRound(el.x) == 40);
+ QVERIFY(qRound(el.y) == 60);
+}
+
+void tst_BMPath::testAnimatedRotoInitialSecondPoint()
+{
+ loadTestData("freeform_roto_animated.json");
+ updateProperty(0);
+
+ QPainterPath::Element el = m_path->path().elementAt(1);
+ QVERIFY(qRound(el.x) == 45);
+ QVERIFY(qRound(el.y) == 65);
+}
+
+void tst_BMPath::testAnimatedRotoInitialThirdPoint()
+{
+ loadTestData("freeform_roto_animated.json");
+ updateProperty(0);
+
+ QPainterPath::Element el = m_path->path().elementAt(2);
+ QVERIFY(qRound(el.x) == 55);
+ QVERIFY(qRound(el.y) == 65);
+}
+
+void tst_BMPath::testAnimatedRotoInitialFourthPoint()
+{
+ loadTestData("freeform_roto_animated.json");
+ updateProperty(0);
+
+ QPainterPath::Element el = m_path->path().elementAt(3);
+ QVERIFY(qRound(el.x) == 60);
+ QVERIFY(qRound(el.y) == 60);
+}
+
+void tst_BMPath::testAnimatedRotoInitialFifthPoint()
+{
+ loadTestData("freeform_roto_animated.json");
+ updateProperty(0);
+
+ QPainterPath::Element el = m_path->path().elementAt(4);
+ QVERIFY(qRound(el.x) == 65);
+ QVERIFY(qRound(el.y) == 55);
+}
+
+void tst_BMPath::testAnimatedRotoInitialSixthPoint()
+{
+ loadTestData("freeform_roto_animated.json");
+ updateProperty(0);
+
+ QPainterPath::Element el = m_path->path().elementAt(5);
+ QVERIFY(qRound(el.x) == 65);
+ QVERIFY(qRound(el.y) == 45);
+}
+
+void tst_BMPath::testAnimatedRotoInitialSeventhPoint()
+{
+ loadTestData("freeform_roto_animated.json");
+ updateProperty(0);
+
+ QPainterPath::Element el = m_path->path().elementAt(6);
+ QVERIFY(qRound(el.x) == 60);
+ QVERIFY(qRound(el.y) == 40);
+}
+
+void tst_BMPath::testAnimatedRotoInitialEighthPoint()
+{
+ loadTestData("freeform_roto_animated.json");
+ updateProperty(0);
+
+ QPainterPath::Element el = m_path->path().elementAt(7);
+ QVERIFY(qRound(el.x) == 55);
+ QVERIFY(qRound(el.y) == 35);
+}
+
+void tst_BMPath::testAnimatedRotoInitialNinthPoint()
+{
+ loadTestData("freeform_roto_animated.json");
+ updateProperty(0);
+
+ QPainterPath::Element el = m_path->path().elementAt(8);
+ QVERIFY(qRound(el.x) == 45);
+ QVERIFY(qRound(el.y) == 35);
+}
+
+void tst_BMPath::testAnimatedRotoInitialTenthPoint()
+{
+ loadTestData("freeform_roto_animated.json");
+ updateProperty(0);
+
+ QPainterPath::Element el = m_path->path().elementAt(9);
+ QVERIFY(qRound(el.x) == 40);
+ QVERIFY(qRound(el.y) == 40);
+}
+
+void tst_BMPath::testAnimatedRotoInitialEleventhPoint()
+{
+ loadTestData("freeform_roto_animated.json");
+ updateProperty(0);
+
+ QPainterPath::Element el = m_path->path().elementAt(10);
+ QVERIFY(qRound(el.x) == 35);
+ QVERIFY(qRound(el.y) == 45);
+}
+
+void tst_BMPath::testAnimatedRotoInitialTwelvthPoint()
+{
+ loadTestData("freeform_roto_animated.json");
+ updateProperty(0);
+
+ QPainterPath::Element el = m_path->path().elementAt(11);
+ QVERIFY(qRound(el.x) == 35);
+ QVERIFY(qRound(el.y) == 55);
+}
+
+void tst_BMPath::testAnimatedRotoInitialClosed()
+{
+ loadTestData("freeform_roto_animated.json");
+ updateProperty(0);
+
+ QPainterPath::Element el1 = m_path->path().elementAt(0);
+ QPainterPath::Element el2 = m_path->path().elementAt(m_path->path().elementCount()-1);
+ QVERIFY(qFuzzyCompare(el1.x, el2.x));
+ QVERIFY(qFuzzyCompare(el1.y, el2.y));
+}
+
+void tst_BMPath::testAnimatedRotoUpdatedStartPoint()
+{
+ loadTestData("freeform_roto_animated.json");
+ updateProperty(179);
+
+ QPainterPath::Element el = m_path->path().elementAt(0);
+ QVERIFY(qRound(el.x) == 40);
+ QVERIFY(qRound(el.y) == 60);
+}
+
+void tst_BMPath::testAnimatedRotoUpdatedSecondPoint()
+{
+ loadTestData("freeform_roto_animated.json");
+ updateProperty(179);
+
+ QPainterPath::Element el = m_path->path().elementAt(1);
+ QVERIFY(qRound(el.x) == 45);
+ QVERIFY(qRound(el.y) == 65);
+}
+
+void tst_BMPath::testAnimatedRotoUpdatedThirdPoint()
+{
+ loadTestData("freeform_roto_animated.json");
+ updateProperty(179);
+
+ QPainterPath::Element el = m_path->path().elementAt(2);
+ QVERIFY(qRound(el.x) == 55);
+ QVERIFY(qRound(el.y) == 65);
+}
+
+void tst_BMPath::testAnimatedRotoUpdatedFourthPoint()
+{
+ loadTestData("freeform_roto_animated.json");
+ updateProperty(179);
+
+ QPainterPath::Element el = m_path->path().elementAt(3);
+ QVERIFY(qRound(el.x) == 60);
+ QVERIFY(qRound(el.y) == 60);
+}
+
+void tst_BMPath::testAnimatedRotoUpdatedFifthPoint()
+{
+ loadTestData("freeform_roto_animated.json");
+ updateProperty(179);
+
+ QPainterPath::Element el = m_path->path().elementAt(4);
+ QVERIFY(qRound(el.x) == 65);
+ QVERIFY(qRound(el.y) == 55);
+}
+
+void tst_BMPath::testAnimatedRotoUpdatedSixthPoint()
+{
+ loadTestData("freeform_roto_animated.json");
+ updateProperty(179);
+
+ QPainterPath::Element el = m_path->path().elementAt(5);
+ QVERIFY(qRound(el.x) == 65);
+ QVERIFY(qRound(el.y) == 45);
+}
+
+void tst_BMPath::testAnimatedRotoUpdatedSeventhPoint()
+{
+ loadTestData("freeform_roto_animated.json");
+ updateProperty(179);
+
+ QPainterPath::Element el = m_path->path().elementAt(6);
+ QVERIFY(qRound(el.x) == 60);
+ QVERIFY(qRound(el.y) == 40);
+}
+
+void tst_BMPath::testAnimatedRotoUpdatedEighthPoint()
+{
+ loadTestData("freeform_roto_animated.json");
+ updateProperty(179);
+
+ QPainterPath::Element el = m_path->path().elementAt(7);
+ QVERIFY(qRound(el.x) == 57);
+ QVERIFY(qRound(el.y) == 37);
+}
+
+void tst_BMPath::testAnimatedRotoUpdatedNinthPoint()
+{
+ loadTestData("freeform_roto_animated.json");
+ updateProperty(179);
+
+ QPainterPath::Element el = m_path->path().elementAt(8);
+ QVERIFY(qRound(el.x) == 53);
+ QVERIFY(qRound(el.y) == 47);
+}
+
+void tst_BMPath::testAnimatedRotoUpdatedTenthPoint()
+{
+ loadTestData("freeform_roto_animated.json");
+ updateProperty(179);
+
+ QPainterPath::Element el = m_path->path().elementAt(9);
+ QVERIFY(qRound(el.x) == 50);
+ QVERIFY(qRound(el.y) == 50);
+}
+
+void tst_BMPath::testAnimatedRotoUpdatedEleventhPoint()
+{
+ loadTestData("freeform_roto_animated.json");
+ updateProperty(179);
+
+ QPainterPath::Element el = m_path->path().elementAt(10);
+ QVERIFY(qRound(el.x) == 47);
+ QVERIFY(qRound(el.y) == 53);
+}
+
+void tst_BMPath::testAnimatedRotoUpdatedTwelvthPoint()
+{
+ loadTestData("freeform_roto_animated.json");
+ updateProperty(179);
+
+ QPainterPath::Element el = m_path->path().elementAt(11);
+ QVERIFY(qRound(el.x) == 37);
+ QVERIFY(qRound(el.y) == 57);
+}
+
+void tst_BMPath::testAnimatedRotoUpdatedClosed()
+{
+ loadTestData("freeform_roto_animated.json");
+ updateProperty(179);
+
+ QPainterPath::Element el1 = m_path->path().elementAt(0);
+ QPainterPath::Element el2 = m_path->path().elementAt(m_path->path().elementCount()-1);
+ QVERIFY(qFuzzyCompare(el1.x, el2.x));
+ QVERIFY(qFuzzyCompare(el1.y, el2.y));
+}
+
+void tst_BMPath::testName()
+{
+ loadTestData("freeform_curve_static.json");
+ QVERIFY(m_path->name() == QString("Path 1"));
+}
+
+void tst_BMPath::testType()
+{
+ loadTestData("freeform_curve_static.json");
+ QVERIFY(m_path->type() == BM_SHAPE_SHAPE_IX);
+}
+
+void tst_BMPath::testActive()
+{
+ loadTestData("freeform_curve_static.json");
+ QVERIFY(m_path->active(100) == true);
+
+ loadTestData("freeform_hidden.json");
+ QVERIFY(m_path->active(100) == false);
+}
+
+void tst_BMPath::testHidden()
+{
+ loadTestData("freeform_hidden.json");
+ QVERIFY(m_path->hidden() == true);
+}
+
+void tst_BMPath::testDirection()
+{
+ loadTestData("freeform_hidden.json");
+ QVERIFY(m_path->direction() == 0);
+ loadTestData("freeform_direction.json");
+ QVERIFY(m_path->direction() == 0);
+}
+
+void tst_BMPath::loadTestData(const QByteArray &filename)
+{
+ if (m_path) {
+ delete m_path;
+ m_path = nullptr;
+ }
+
+ QFile sourceFile(QFINDTESTDATA(filename.constData()));
+ if (!sourceFile.exists())
+ QFAIL("File does not exist");
+ if (!sourceFile.open(QIODevice::ReadOnly))
+ QFAIL("Cannot read test file");
+
+ QByteArray json = sourceFile.readAll();
+
+ sourceFile.close();
+
+ QJsonDocument doc = QJsonDocument::fromJson(json);
+ QJsonObject rootObj = doc.object();
+ if (rootObj.empty())
+ QFAIL("Cannot parse test file");
+
+ QJsonArray layers = rootObj.value(QLatin1String("layers")).toArray();
+ QJsonObject layerObj = layers[0].toObject();
+ int type = layerObj.value(QLatin1String("ty")).toInt();
+ if (type != 4)
+ QFAIL("It's not shape layer");
+
+ QJsonArray shapes = layerObj.value(QLatin1String("shapes")).toArray();
+ QJsonArray::const_iterator shapesIt = shapes.constBegin();
+ BMShape* shape = nullptr;
+ while (shapesIt != shapes.end()) {
+ QJsonObject childObj = (*shapesIt).toObject();
+ shape = BMShape::construct(childObj);
+ QVERIFY(shape != nullptr);
+ if (shape->type() == BM_SHAPE_SHAPE_IX)
+ break;
+ shapesIt++;
+ }
+
+ m_path = static_cast<BMFreeFormShape*>(shape);
+
+ QVERIFY(m_path != nullptr);
+}
+
+void tst_BMPath::updateProperty(int frame)
+{
+ m_path->updateProperties(frame);
+}
+
+QTEST_MAIN(tst_BMPath)
+#include "tst_bmpath.moc"
diff --git a/tests/auto/bodymovin/shape/rect/rect.pro b/tests/auto/bodymovin/shape/rect/rect.pro
new file mode 100644
index 0000000..75a356e
--- /dev/null
+++ b/tests/auto/bodymovin/shape/rect/rect.pro
@@ -0,0 +1,5 @@
+QT += testlib gui-private bodymovin-private
+CONFIG += c++11
+
+SOURCES = \
+ tst_bmrect.cpp
diff --git a/tests/auto/bodymovin/shape/rect/rect_animated_30x30_1515_rad0_to_50x50_7575_rad25.json b/tests/auto/bodymovin/shape/rect/rect_animated_30x30_1515_rad0_to_50x50_7575_rad25.json
new file mode 100644
index 0000000..c2fc75e
--- /dev/null
+++ b/tests/auto/bodymovin/shape/rect/rect_animated_30x30_1515_rad0_to_50x50_7575_rad25.json
@@ -0,0 +1 @@
+{"v":"5.3.4","fr":60,"ip":0,"op":180,"w":100,"h":100,"nm":"AnimatedRectShapeLayer","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,0,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"rc","d":1,"s":{"a":1,"k":[{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"n":["0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167"],"t":0,"s":[30,30],"e":[50,50]},{"t":179}],"ix":2},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":0,"s":[15,15],"e":[75,75],"to":[10,10],"ti":[-10,-10]},{"t":179}],"ix":3},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":0,"s":[0],"e":[25]},{"t":179}],"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false}],"ip":0,"op":180,"st":0,"bm":0}],"markers":[]}
diff --git a/tests/auto/bodymovin/shape/rect/rect_direction.json b/tests/auto/bodymovin/shape/rect/rect_direction.json
new file mode 100644
index 0000000..102834a
--- /dev/null
+++ b/tests/auto/bodymovin/shape/rect/rect_direction.json
@@ -0,0 +1 @@
+{"v":"5.3.4","fr":60,"ip":0,"op":180,"w":100,"h":100,"nm":"StaticRectShapeLayer","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,0,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"rc","d":3,"s":{"a":0,"k":[30,30],"ix":2},"p":{"a":0,"k":[50,50],"ix":3},"r":{"a":0,"k":0,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false}],"ip":60,"op":120,"st":0,"bm":0}],"markers":[]}
diff --git a/tests/auto/bodymovin/shape/rect/rect_hidden.json b/tests/auto/bodymovin/shape/rect/rect_hidden.json
new file mode 100644
index 0000000..902190d
--- /dev/null
+++ b/tests/auto/bodymovin/shape/rect/rect_hidden.json
@@ -0,0 +1 @@
+{"v":"5.3.4","fr":60,"ip":0,"op":180,"w":100,"h":100,"nm":"HiddenRectShapeLayer","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,0,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"rc","d":1,"s":{"a":0,"k":[30,30],"ix":2},"p":{"a":0,"k":[50,50],"ix":3},"r":{"a":0,"k":0,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":true}],"ip":0,"op":180,"st":0,"bm":0}],"markers":[]}
diff --git a/tests/auto/bodymovin/shape/rect/rect_static_30x30_5050_rad0.json b/tests/auto/bodymovin/shape/rect/rect_static_30x30_5050_rad0.json
new file mode 100644
index 0000000..58d82cf
--- /dev/null
+++ b/tests/auto/bodymovin/shape/rect/rect_static_30x30_5050_rad0.json
@@ -0,0 +1 @@
+{"v":"5.3.4","fr":60,"ip":0,"op":180,"w":100,"h":100,"nm":"StaticRectShapeLayer","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,0,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"rc","d":1,"s":{"a":0,"k":[30,30],"ix":2},"p":{"a":0,"k":[50,50],"ix":3},"r":{"a":0,"k":0,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false}],"ip":0,"op":180,"st":0,"bm":0}],"markers":[]}
diff --git a/tests/auto/bodymovin/shape/rect/tst_bmrect.cpp b/tests/auto/bodymovin/shape/rect/tst_bmrect.cpp
new file mode 100644
index 0000000..abec892
--- /dev/null
+++ b/tests/auto/bodymovin/shape/rect/tst_bmrect.cpp
@@ -0,0 +1,349 @@
+/****************************************************************************
+**
+** Copyright (C) 2018 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the lottie-qt module of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:GPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 or (at your option) any later version
+** approved by the KDE Free Qt Foundation. The licenses are as published by
+** the Free Software Foundation and appearing in the file LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#include <QtTest/QtTest>
+
+#include <QFile>
+#include <QJsonDocument>
+#include <QJsonObject>
+#include <QJsonArray>
+
+#include "private/bmlayer_p.h"
+#include "private/bmrect_p.h"
+
+class tst_BMRect: public QObject
+{
+ Q_OBJECT
+
+public:
+ tst_BMRect();
+ ~tst_BMRect();
+
+private slots:
+ void initTestCase();
+ void cleanupTestCase();
+
+ void testStaticInitialX();
+ void testStaticInitialY();
+ void testStaticInitialWidth();
+ void testStaticInitialHeight();
+ void testStaticInitialRoundness();
+ void testStaticUpdatedX();
+ void testStaticUpdatedY();
+ void testStaticUpdatedWidth();
+ void testStaticUpdatedHeight();
+ void testStaticUpdatedRoundness();
+
+ void testAnimatedInitialX();
+ void testAnimatedInitialY();
+ void testAnimatedInitialWidth();
+ void testAnimatedInitialHeight();
+ void testAnimatedInitialRoundness();
+ void testAnimatedUpdatedX();
+ void testAnimatedUpdatedY();
+ void testAnimatedUpdatedWidth();
+ void testAnimatedUpdatedHeight();
+ void testAnimatedUpdatedRoundness();
+
+ void testName();
+ void testType();
+ void testHidden();
+ void testActive();
+ void testDirection();
+
+private:
+ void loadTestData(const QByteArray &filename);
+ void updateProperty(int frame);
+
+ BMRect *m_rect = nullptr;
+};
+
+tst_BMRect::tst_BMRect()
+{
+
+}
+
+tst_BMRect::~tst_BMRect()
+{
+
+}
+
+void tst_BMRect::initTestCase()
+{
+}
+
+void tst_BMRect::cleanupTestCase()
+{
+ if (m_rect)
+ delete m_rect;
+}
+
+void tst_BMRect::testStaticInitialX()
+{
+ loadTestData("rect_static_30x30_5050_rad0.json");
+
+ QVERIFY(qFuzzyCompare(m_rect->position().x(), 50.0));
+}
+
+void tst_BMRect:: testStaticInitialY()
+{
+ loadTestData("rect_static_30x30_5050_rad0.json");
+
+ QVERIFY(qFuzzyCompare(m_rect->position().y(), 50.0));
+}
+
+void tst_BMRect::testStaticInitialWidth()
+{
+ loadTestData("rect_static_30x30_5050_rad0.json");
+
+ QVERIFY(qFuzzyCompare(m_rect->size().width(), 30.0));
+}
+
+void tst_BMRect::testStaticInitialHeight()
+{
+ loadTestData("rect_static_30x30_5050_rad0.json");
+
+ QVERIFY(qFuzzyCompare(m_rect->size().height(), 30.0));
+}
+
+void tst_BMRect:: testStaticInitialRoundness()
+{
+ loadTestData("rect_static_30x30_5050_rad0.json");
+
+ QVERIFY(qFuzzyCompare(m_rect->roundness(), 0.0));
+}
+
+void tst_BMRect::testStaticUpdatedX()
+{
+ loadTestData("rect_static_30x30_5050_rad0.json");
+ updateProperty(179);
+
+ QVERIFY(qFuzzyCompare(m_rect->position().x(), 50.0));
+}
+
+void tst_BMRect:: testStaticUpdatedY()
+{
+ loadTestData("rect_static_30x30_5050_rad0.json");
+ updateProperty(179);
+
+ QVERIFY(qFuzzyCompare(m_rect->position().y(), 50.0));
+}
+
+void tst_BMRect::testStaticUpdatedWidth()
+{
+ loadTestData("rect_static_30x30_5050_rad0.json");
+ updateProperty(179);
+
+ QVERIFY(qFuzzyCompare(m_rect->size().width(), 30.0));
+}
+
+void tst_BMRect::testStaticUpdatedHeight()
+{
+ loadTestData("rect_static_30x30_5050_rad0.json");
+ updateProperty(179);
+
+ QVERIFY(qFuzzyCompare(m_rect->size().height(), 30.0));
+}
+
+void tst_BMRect:: testStaticUpdatedRoundness()
+{
+ loadTestData("rect_static_30x30_5050_rad0.json");
+ updateProperty(179);
+
+ QVERIFY(qFuzzyCompare(m_rect->roundness(), 0.0));
+}
+
+void tst_BMRect::testAnimatedInitialX()
+{
+ loadTestData("rect_animated_30x30_1515_rad0_to_50x50_7575_rad25.json");
+ updateProperty(0);
+
+ QVERIFY(qFuzzyCompare(m_rect->position().x(), 15.0));
+}
+
+void tst_BMRect:: testAnimatedInitialY()
+{
+ loadTestData("rect_animated_30x30_1515_rad0_to_50x50_7575_rad25.json");
+ updateProperty(0);
+
+ QVERIFY(qFuzzyCompare(m_rect->position().y(), 15.0));
+}
+
+void tst_BMRect::testAnimatedInitialWidth()
+{
+ loadTestData("rect_animated_30x30_1515_rad0_to_50x50_7575_rad25.json");
+ updateProperty(0);
+
+ QVERIFY(qFuzzyCompare(m_rect->size().width(), 30.0));
+}
+
+void tst_BMRect::testAnimatedInitialHeight()
+{
+ loadTestData("rect_animated_30x30_1515_rad0_to_50x50_7575_rad25.json");
+ updateProperty(0);
+
+ QVERIFY(qFuzzyCompare(m_rect->size().height(), 30.0));
+}
+
+void tst_BMRect::testAnimatedInitialRoundness()
+{
+ loadTestData("rect_animated_30x30_1515_rad0_to_50x50_7575_rad25.json");
+ updateProperty(0);
+
+ QVERIFY(qFuzzyCompare(m_rect->roundness(), 0.0));
+}
+
+void tst_BMRect::testAnimatedUpdatedX()
+{
+ loadTestData("rect_animated_30x30_1515_rad0_to_50x50_7575_rad25.json");
+ updateProperty(179);
+
+ QVERIFY(qFuzzyCompare(m_rect->position().x(), 75.0));
+}
+
+void tst_BMRect:: testAnimatedUpdatedY()
+{
+ loadTestData("rect_animated_30x30_1515_rad0_to_50x50_7575_rad25.json");
+ updateProperty(179);
+
+ QVERIFY(qFuzzyCompare(m_rect->position().y(), 75.0));
+}
+
+void tst_BMRect::testAnimatedUpdatedWidth()
+{
+ loadTestData("rect_animated_30x30_1515_rad0_to_50x50_7575_rad25.json");
+ updateProperty(179);
+
+ QVERIFY(qFuzzyCompare(m_rect->size().width(), 50.0));
+}
+
+void tst_BMRect::testAnimatedUpdatedHeight()
+{
+ loadTestData("rect_animated_30x30_1515_rad0_to_50x50_7575_rad25.json");
+ updateProperty(179);
+
+ QVERIFY(qFuzzyCompare(m_rect->size().height(), 50.0));
+}
+
+void tst_BMRect::testAnimatedUpdatedRoundness()
+{
+ loadTestData("rect_animated_30x30_1515_rad0_to_50x50_7575_rad25.json");
+ updateProperty(179);
+
+ QVERIFY(qFuzzyCompare(m_rect->roundness(), 25.0));
+}
+
+void tst_BMRect::testName()
+{
+ loadTestData("rect_static_30x30_5050_rad0.json");
+ QVERIFY(m_rect->name() == QString("Rectangle Path 1"));
+}
+
+void tst_BMRect::testType()
+{
+ loadTestData("rect_static_30x30_5050_rad0.json");
+ QVERIFY(m_rect->type() == BM_SHAPE_RECT_IX);
+}
+
+void tst_BMRect::testHidden()
+{
+ loadTestData("rect_hidden.json");
+ QVERIFY(m_rect->hidden() == true);
+}
+
+void tst_BMRect::testActive()
+{
+ loadTestData("rect_hidden.json");
+ QVERIFY(m_rect->active(100) == false);
+
+ loadTestData("rect_static_30x30_5050_rad0.json");
+ QVERIFY(m_rect->active(100) == true);
+}
+
+void tst_BMRect::testDirection()
+{
+ loadTestData("rect_hidden.json");
+ QVERIFY(m_rect->direction() == 0);
+
+ loadTestData("rect_direction.json");
+ QVERIFY(m_rect->direction() == 3);
+}
+
+void tst_BMRect::loadTestData(const QByteArray &filename)
+{
+ if (m_rect) {
+ delete m_rect;
+ m_rect = nullptr;
+ }
+
+ QFile sourceFile(QFINDTESTDATA(filename.constData()));
+ if (!sourceFile.exists())
+ QFAIL("File does not exist");
+ if (!sourceFile.open(QIODevice::ReadOnly))
+ QFAIL("Cannot read test file");
+
+ QByteArray json = sourceFile.readAll();
+
+ sourceFile.close();
+
+ QJsonDocument doc = QJsonDocument::fromJson(json);
+ QJsonObject rootObj = doc.object();
+ if (rootObj.empty())
+ QFAIL("Cannot parse test file");
+
+ QJsonArray layers = rootObj.value(QLatin1String("layers")).toArray();
+ QJsonObject layerObj = layers[0].toObject();
+ int type = layerObj.value(QLatin1String("ty")).toInt();
+ if (type != 4)
+ QFAIL("It's not shape layer");
+
+ QJsonArray shapes = layerObj.value(QLatin1String("shapes")).toArray();
+ QJsonArray::const_iterator shapesIt = shapes.constBegin();
+ BMShape* shape = nullptr;
+ while (shapesIt != shapes.end()) {
+ QJsonObject childObj = (*shapesIt).toObject();
+ shape = BMShape::construct(childObj);
+ QVERIFY(shape != nullptr);
+ if (shape->type() == BM_SHAPE_RECT_IX)
+ break;
+ shapesIt++;
+ }
+
+ m_rect = static_cast<BMRect*>(shape);
+
+ QVERIFY(m_rect != nullptr);
+}
+
+void tst_BMRect::updateProperty(int frame)
+{
+ m_rect->updateProperties(frame);
+}
+
+QTEST_MAIN(tst_BMRect)
+#include "tst_bmrect.moc"
diff --git a/tests/auto/bodymovin/shape/repeater/repeater.pro b/tests/auto/bodymovin/shape/repeater/repeater.pro
new file mode 100644
index 0000000..646838b
--- /dev/null
+++ b/tests/auto/bodymovin/shape/repeater/repeater.pro
@@ -0,0 +1,5 @@
+QT += testlib gui-private bodymovin-private
+CONFIG += c++11
+
+SOURCES = \
+ tst_bmrepeater.cpp
diff --git a/tests/auto/bodymovin/shape/repeater/repeater_animated.json b/tests/auto/bodymovin/shape/repeater/repeater_animated.json
new file mode 100644
index 0000000..70a6378
--- /dev/null
+++ b/tests/auto/bodymovin/shape/repeater/repeater_animated.json
@@ -0,0 +1 @@
+{"v":"5.3.4","fr":60,"ip":0,"op":180,"w":100,"h":100,"nm":"AnimatedRepeaterShapeLayer","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,0,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"rp","c":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":0,"s":[3],"e":[30]},{"t":179}],"ix":1},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":0,"s":[0],"e":[15]},{"t":179}],"ix":2},"m":1,"ix":1,"tr":{"ty":"tr","p":{"a":0,"k":[100,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":4},"so":{"a":0,"k":100,"ix":5},"eo":{"a":0,"k":100,"ix":6},"nm":"Transform"},"nm":"Repeater 1","mn":"ADBE Vector Filter - Repeater","hd":false}],"ip":-20,"op":184,"st":0,"bm":0}],"markers":[]}
diff --git a/tests/auto/bodymovin/shape/repeater/repeater_hidden.json b/tests/auto/bodymovin/shape/repeater/repeater_hidden.json
new file mode 100644
index 0000000..c5ee464
--- /dev/null
+++ b/tests/auto/bodymovin/shape/repeater/repeater_hidden.json
@@ -0,0 +1 @@
+{"v":"5.3.4","fr":60,"ip":0,"op":180,"w":100,"h":100,"nm":"StaticRepeaterShapeLayer","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,0,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"rp","c":{"a":0,"k":3,"ix":1},"o":{"a":0,"k":0,"ix":2},"m":1,"ix":1,"tr":{"ty":"tr","p":{"a":0,"k":[100,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":4},"so":{"a":0,"k":100,"ix":5},"eo":{"a":0,"k":100,"ix":6},"nm":"Transform"},"nm":"Repeater 1","mn":"ADBE Vector Filter - Repeater","hd":true}],"ip":-50,"op":184,"st":0,"bm":0}],"markers":[]}
diff --git a/tests/auto/bodymovin/shape/repeater/repeater_static.json b/tests/auto/bodymovin/shape/repeater/repeater_static.json
new file mode 100644
index 0000000..a7f5067
--- /dev/null
+++ b/tests/auto/bodymovin/shape/repeater/repeater_static.json
@@ -0,0 +1 @@
+{"v":"5.3.4","fr":60,"ip":0,"op":180,"w":100,"h":100,"nm":"StaticRepeaterShapeLayer","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,0,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"rp","c":{"a":0,"k":3,"ix":1},"o":{"a":0,"k":0,"ix":2},"m":1,"ix":1,"tr":{"ty":"tr","p":{"a":0,"k":[100,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":4},"so":{"a":0,"k":100,"ix":5},"eo":{"a":0,"k":100,"ix":6},"nm":"Transform"},"nm":"Repeater 1","mn":"ADBE Vector Filter - Repeater","hd":false}],"ip":-50,"op":184,"st":0,"bm":0}],"markers":[]}
diff --git a/tests/auto/bodymovin/shape/repeater/tst_bmrepeater.cpp b/tests/auto/bodymovin/shape/repeater/tst_bmrepeater.cpp
new file mode 100644
index 0000000..bbb49cb
--- /dev/null
+++ b/tests/auto/bodymovin/shape/repeater/tst_bmrepeater.cpp
@@ -0,0 +1,231 @@
+/****************************************************************************
+**
+** Copyright (C) 2018 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the lottie-qt module of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:GPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 or (at your option) any later version
+** approved by the KDE Free Qt Foundation. The licenses are as published by
+** the Free Software Foundation and appearing in the file LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#include <QtTest/QtTest>
+
+#include <QFile>
+#include <QJsonDocument>
+#include <QJsonObject>
+#include <QJsonArray>
+
+#include "private/bmlayer_p.h"
+#include "private/bmrepeater_p.h"
+
+class tst_BMRepeater : public QObject
+{
+ Q_OBJECT
+
+public:
+ tst_BMRepeater();
+ ~tst_BMRepeater();
+
+private slots:
+ void initTestCase();
+ void cleanupTestCase();
+
+ void testStaticInitialCopy();
+ void testStaticInitialOffset();
+ void testStaticUpdatedCopy();
+ void testStaticUpdatedOffset();
+
+ void testAnimatedInitialCopy();
+ void testAnimatedInitialOffset();
+ void testAnimatedUpdatedCopy();
+ void testAnimatedUpdatedOffset();
+
+ void testName();
+ void testType();
+ void testHidden();
+ void testActive();
+
+private:
+ void loadTestData(const QByteArray &filename);
+ void updateProperty(int frame);
+
+ BMRepeater *m_repeater = nullptr;
+
+};
+
+tst_BMRepeater::tst_BMRepeater()
+{
+
+}
+
+tst_BMRepeater::~tst_BMRepeater()
+{
+
+}
+
+void tst_BMRepeater::initTestCase()
+{
+
+}
+
+void tst_BMRepeater::cleanupTestCase()
+{
+
+}
+
+void tst_BMRepeater::testStaticInitialCopy()
+{
+ loadTestData("repeater_static.json");
+ QVERIFY(m_repeater->copies() == 3);
+}
+
+void tst_BMRepeater::testStaticInitialOffset()
+{
+ loadTestData("repeater_static.json");
+ QVERIFY(qFuzzyCompare(m_repeater->offset(), 0.0));
+}
+
+void tst_BMRepeater::testStaticUpdatedCopy()
+{
+ loadTestData("repeater_static.json");
+ updateProperty(180);
+ QVERIFY(m_repeater->copies() == 3);
+}
+
+void tst_BMRepeater::testStaticUpdatedOffset()
+{
+ loadTestData("repeater_static.json");
+ updateProperty(180);
+ QVERIFY(qFuzzyCompare(m_repeater->offset(), 0.0));
+}
+
+void tst_BMRepeater::testAnimatedInitialCopy()
+{
+ loadTestData("repeater_animated.json");
+ updateProperty(0);
+ QVERIFY(m_repeater->copies() == 3);
+}
+
+void tst_BMRepeater::testAnimatedInitialOffset()
+{
+ loadTestData("repeater_animated.json");
+ updateProperty(0);
+ QVERIFY(qFuzzyCompare(m_repeater->offset(), 0.0));
+}
+
+void tst_BMRepeater::testAnimatedUpdatedCopy()
+{
+ loadTestData("repeater_animated.json");
+ updateProperty(180);
+ QVERIFY(m_repeater->copies() == 30);
+}
+
+void tst_BMRepeater::testAnimatedUpdatedOffset()
+{
+ loadTestData("repeater_animated.json");
+ updateProperty(180);
+ QVERIFY(qFuzzyCompare(m_repeater->offset(), 15.0));
+}
+
+void tst_BMRepeater::testName()
+{
+ loadTestData("repeater_static.json");
+ QVERIFY(m_repeater->name() == QString("Repeater 1"));
+}
+
+void tst_BMRepeater::testType()
+{
+ loadTestData("repeater_static.json");
+ QVERIFY(m_repeater->type() == BM_SHAPE_REPEATER_IX);
+}
+
+void tst_BMRepeater::testActive()
+{
+ loadTestData("repeater_static.json");
+ QVERIFY(m_repeater->active(100) == true);
+
+ loadTestData("repeater_hidden.json");
+ QVERIFY(m_repeater->active(100) == false);
+}
+
+void tst_BMRepeater::testHidden()
+{
+ loadTestData("repeater_static.json");
+ QVERIFY(m_repeater->hidden() == false);
+
+ loadTestData("repeater_hidden.json");
+ QVERIFY(m_repeater->hidden() == true);
+}
+
+
+void tst_BMRepeater::loadTestData(const QByteArray &filename)
+{
+ if (m_repeater) {
+ delete m_repeater;
+ m_repeater = nullptr;
+ }
+
+ QFile sourceFile(QFINDTESTDATA(filename.constData()));
+ if (!sourceFile.exists())
+ QFAIL("File does not exist");
+ if (!sourceFile.open(QIODevice::ReadOnly))
+ QFAIL("Cannot read test file");
+
+ QByteArray json = sourceFile.readAll();
+
+ sourceFile.close();
+
+ QJsonDocument doc = QJsonDocument::fromJson(json);
+ QJsonObject rootObj = doc.object();
+ if (rootObj.empty())
+ QFAIL("Cannot parse test file");
+
+ QJsonArray layers = rootObj.value(QLatin1String("layers")).toArray();
+ QJsonObject layerObj = layers[0].toObject();
+ int type = layerObj.value(QLatin1String("ty")).toInt();
+ if (type != 4)
+ QFAIL("It's not shape layer");
+
+ QJsonArray shapes = layerObj.value(QLatin1String("shapes")).toArray();
+ QJsonArray::const_iterator shapesIt = shapes.constBegin();
+ BMShape* shape = nullptr;
+ while (shapesIt != shapes.end()) {
+ QJsonObject childObj = (*shapesIt).toObject();
+ shape = BMShape::construct(childObj);
+ QVERIFY(shape != nullptr);
+ if (shape->type() == BM_SHAPE_REPEATER_IX)
+ break;
+ shapesIt++;
+ }
+
+ m_repeater = static_cast<BMRepeater*>(shape);
+
+ QVERIFY(m_repeater != nullptr);
+}
+
+void tst_BMRepeater::updateProperty(int frame)
+{
+ m_repeater->updateProperties(frame);
+}
+
+QTEST_MAIN(tst_BMRepeater)
+#include "tst_bmrepeater.moc"
diff --git a/tests/auto/bodymovin/shape/repeatertransform/repeater_transform_animated.json b/tests/auto/bodymovin/shape/repeatertransform/repeater_transform_animated.json
new file mode 100644
index 0000000..ddaf233
--- /dev/null
+++ b/tests/auto/bodymovin/shape/repeatertransform/repeater_transform_animated.json
@@ -0,0 +1 @@
+{"v":"5.3.4","fr":60,"ip":0,"op":180,"w":100,"h":100,"nm":"AnimatedRepeaterTransformShapeLayer","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,0,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"rp","c":{"a":0,"k":3,"ix":1},"o":{"a":0,"k":0,"ix":2},"m":1,"ix":1,"tr":{"ty":"tr","p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":0,"s":[50,50],"e":[75,75],"to":[4.16666650772095,4.16666650772095],"ti":[-4.16666650772095,-4.16666650772095]},{"t":179}],"ix":2},"a":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":0,"s":[50,50],"e":[25,25],"to":[-4.16666650772095,-4.16666650772095],"ti":[4.16666650772095,4.16666650772095]},{"t":179}],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"n":["0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167"],"t":0,"s":[50,50],"e":[100,100]},{"t":179}],"ix":3},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":0,"s":[0],"e":[1110]},{"t":179}],"ix":4},"so":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":0,"s":[0],"e":[25]},{"t":179}],"ix":5},"eo":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":0,"s":[100],"e":[75]},{"t":179}],"ix":6},"nm":"Transform"},"nm":"Repeater 1","mn":"ADBE Vector Filter - Repeater","hd":false}],"ip":-50,"op":184,"st":0,"bm":0}],"markers":[]}
diff --git a/tests/auto/bodymovin/shape/repeatertransform/repeater_transform_static.json b/tests/auto/bodymovin/shape/repeatertransform/repeater_transform_static.json
new file mode 100644
index 0000000..87d9206
--- /dev/null
+++ b/tests/auto/bodymovin/shape/repeatertransform/repeater_transform_static.json
@@ -0,0 +1 @@
+{"v":"5.3.4","fr":60,"ip":0,"op":180,"w":100,"h":100,"nm":"StaticRepeaterTransformShapeLayer","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,0,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"rp","c":{"a":0,"k":3,"ix":1},"o":{"a":0,"k":0,"ix":2},"m":1,"ix":1,"tr":{"ty":"tr","p":{"a":0,"k":[50,50],"ix":2},"a":{"a":0,"k":[50,50],"ix":1},"s":{"a":0,"k":[50,50],"ix":3},"r":{"a":0,"k":0,"ix":4},"so":{"a":0,"k":100,"ix":5},"eo":{"a":0,"k":100,"ix":6},"nm":"Transform"},"nm":"Repeater 1","mn":"ADBE Vector Filter - Repeater","hd":false}],"ip":-50,"op":184,"st":0,"bm":0}],"markers":[]}
diff --git a/tests/auto/bodymovin/shape/repeatertransform/repeatertransform.pro b/tests/auto/bodymovin/shape/repeatertransform/repeatertransform.pro
new file mode 100644
index 0000000..5306888
--- /dev/null
+++ b/tests/auto/bodymovin/shape/repeatertransform/repeatertransform.pro
@@ -0,0 +1,6 @@
+QT += testlib gui-private bodymovin-private
+CONFIG += c++11
+
+SOURCES = \
+ tst_bmrepeatertransform.cpp
+
diff --git a/tests/auto/bodymovin/shape/repeatertransform/tst_bmrepeatertransform.cpp b/tests/auto/bodymovin/shape/repeatertransform/tst_bmrepeatertransform.cpp
new file mode 100644
index 0000000..fb0ef52
--- /dev/null
+++ b/tests/auto/bodymovin/shape/repeatertransform/tst_bmrepeatertransform.cpp
@@ -0,0 +1,461 @@
+/****************************************************************************
+**
+** Copyright (C) 2018 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the lottie-qt module of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:GPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 or (at your option) any later version
+** approved by the KDE Free Qt Foundation. The licenses are as published by
+** the Free Software Foundation and appearing in the file LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#include <QtTest/QtTest>
+
+#include <QFile>
+#include <QJsonDocument>
+#include <QJsonObject>
+#include <QJsonArray>
+
+#include "private/bmlayer_p.h"
+#include "private/bmrepeater_p.h"
+#include "private/bmrepeatertransform_p.h"
+
+class tst_BMRepeaterTransform: public QObject
+{
+ Q_OBJECT
+
+public:
+ tst_BMRepeaterTransform();
+ ~tst_BMRepeaterTransform();
+
+private:
+
+ // void testParseStaticRect();
+
+private slots:
+ void initTestCase();
+ void cleanupTestCase();
+
+ void testStaticInitialAnchorX();
+ void testStaticInitialAnchorY();
+ void testStaticInitialPositionX();
+ void testStaticInitialPositionY();
+ void testStaticInitialScaleX();
+ void testStaticInitialScaleY();
+ void testStaticInitialRotation();
+ void testStaticInitialStartOpacity();
+ void testStaticInitialEndOpacity();
+ void testStaticUpdatedAnchorX();
+ void testStaticUpdatedAnchorY();
+ void testStaticUpdatedPositionX();
+ void testStaticUpdatedPositionY();
+ void testStaticUpdatedScaleX();
+ void testStaticUpdatedScaleY();
+ void testStaticUpdatedRotation();
+ void testStaticUpdatedStartOpacity();
+ void testStaticUpdatedEndOpacity();
+
+ void testAnimatedInitialAnchorX();
+ void testAnimatedInitialAnchorY();
+ void testAnimatedInitialPositionX();
+ void testAnimatedInitialPositionY();
+ void testAnimatedInitialScaleX();
+ void testAnimatedInitialScaleY();
+ void testAnimatedInitialRotation();
+ void testAnimatedInitialStartOpacity();
+ void testAnimatedInitialEndOpacity();
+ void testAnimatedUpdatedAnchorX();
+ void testAnimatedUpdatedAnchorY();
+ void testAnimatedUpdatedPositionX();
+ void testAnimatedUpdatedPositionY();
+ void testAnimatedUpdatedScaleX();
+ void testAnimatedUpdatedScaleY();
+ void testAnimatedUpdatedRotation();
+ void testAnimatedUpdatedStartOpacity();
+ void testAnimatedUpdatedEndOpacity();
+
+ void testActive();
+
+private:
+ void loadTestData(const QByteArray &filename);
+ void updateProperty(int frame);
+
+ BMRepeaterTransform *m_transform = nullptr;
+};
+
+tst_BMRepeaterTransform::tst_BMRepeaterTransform()
+{
+
+}
+
+tst_BMRepeaterTransform::~tst_BMRepeaterTransform()
+{
+
+}
+
+void tst_BMRepeaterTransform::initTestCase()
+{
+}
+
+void tst_BMRepeaterTransform::cleanupTestCase()
+{
+ if (m_transform)
+ delete m_transform;
+}
+
+void tst_BMRepeaterTransform::testStaticInitialAnchorX()
+{
+ loadTestData("repeater_transform_static.json");
+
+ QVERIFY(qFuzzyCompare(m_transform->anchorPoint().x(), 50.0));
+}
+
+void tst_BMRepeaterTransform::testStaticInitialAnchorY()
+{
+ loadTestData("repeater_transform_static.json");
+
+ QVERIFY(qFuzzyCompare(m_transform->anchorPoint().y(), 50.0));
+}
+
+void tst_BMRepeaterTransform::testStaticInitialPositionX()
+{
+ loadTestData("repeater_transform_static.json");
+
+ QVERIFY(qFuzzyCompare(m_transform->position().x(), 50.0));
+}
+
+void tst_BMRepeaterTransform::testStaticInitialPositionY()
+{
+ loadTestData("repeater_transform_static.json");
+
+ QVERIFY(qFuzzyCompare(m_transform->position().y(), 50.0));
+}
+
+void tst_BMRepeaterTransform::testStaticInitialScaleX()
+{
+ loadTestData("repeater_transform_static.json");
+
+ QVERIFY(qFuzzyCompare(m_transform->scale().x(), 0.5));
+}
+
+void tst_BMRepeaterTransform::testStaticInitialScaleY()
+{
+ loadTestData("repeater_transform_static.json");
+
+ QVERIFY(qFuzzyCompare(m_transform->scale().y(), 0.5));
+}
+
+void tst_BMRepeaterTransform::testStaticInitialRotation()
+{
+ loadTestData("repeater_transform_static.json");
+
+ QVERIFY(qFuzzyCompare(m_transform->rotation(), 0.0));
+}
+
+void tst_BMRepeaterTransform::testStaticInitialStartOpacity()
+{
+ loadTestData("repeater_transform_static.json");
+
+ QVERIFY(qFuzzyCompare(m_transform->startOpacity(), 100.0));
+}
+
+void tst_BMRepeaterTransform::testStaticInitialEndOpacity()
+{
+ loadTestData("repeater_transform_static.json");
+
+ QVERIFY(qFuzzyCompare(m_transform->endOpacity(), 100.0));
+}
+
+void tst_BMRepeaterTransform::testStaticUpdatedAnchorX()
+{
+ loadTestData("repeater_transform_static.json");
+ updateProperty(180);
+
+ QVERIFY(qFuzzyCompare(m_transform->anchorPoint().x(), 50.0));
+}
+
+void tst_BMRepeaterTransform::testStaticUpdatedAnchorY()
+{
+ loadTestData("repeater_transform_static.json");
+ updateProperty(180);
+
+ QVERIFY(qFuzzyCompare(m_transform->anchorPoint().y(), 50.0));
+}
+
+void tst_BMRepeaterTransform::testStaticUpdatedPositionX()
+{
+ loadTestData("repeater_transform_static.json");
+ updateProperty(180);
+
+ QVERIFY(qFuzzyCompare(m_transform->position().x(), 50.0));
+}
+
+void tst_BMRepeaterTransform::testStaticUpdatedPositionY()
+{
+ loadTestData("repeater_transform_static.json");
+ updateProperty(180);
+
+ QVERIFY(qFuzzyCompare(m_transform->position().y(), 50.0));
+}
+
+void tst_BMRepeaterTransform::testStaticUpdatedScaleX()
+{
+ loadTestData("repeater_transform_static.json");
+ updateProperty(180);
+
+ QVERIFY(qFuzzyCompare(m_transform->scale().x(), 0.5));
+}
+
+void tst_BMRepeaterTransform::testStaticUpdatedScaleY()
+{
+ loadTestData("repeater_transform_static.json");
+ updateProperty(180);
+
+ QVERIFY(qFuzzyCompare(m_transform->scale().y(), 0.5));
+}
+
+void tst_BMRepeaterTransform::testStaticUpdatedRotation()
+{
+ loadTestData("repeater_transform_static.json");
+ updateProperty(180);
+
+ QVERIFY(qFuzzyCompare(m_transform->rotation(), 0.0));
+}
+
+void tst_BMRepeaterTransform::testStaticUpdatedStartOpacity()
+{
+ loadTestData("repeater_transform_static.json");
+ updateProperty(180);
+
+ QVERIFY(qFuzzyCompare(m_transform->startOpacity(), 100.0));
+}
+
+void tst_BMRepeaterTransform::testStaticUpdatedEndOpacity()
+{
+ loadTestData("repeater_transform_static.json");
+ updateProperty(180);
+
+ QVERIFY(qFuzzyCompare(m_transform->endOpacity(), 100.0));
+}
+
+void tst_BMRepeaterTransform::testAnimatedInitialAnchorX()
+{
+ loadTestData("repeater_transform_animated.json");
+ updateProperty(0);
+
+ QVERIFY(qFuzzyCompare(m_transform->anchorPoint().x(), 50.0));
+}
+
+void tst_BMRepeaterTransform::testAnimatedInitialAnchorY()
+{
+ loadTestData("repeater_transform_animated.json");
+ updateProperty(0);
+
+ QVERIFY(qFuzzyCompare(m_transform->anchorPoint().y(), 50.0));
+}
+
+void tst_BMRepeaterTransform::testAnimatedInitialPositionX()
+{
+ loadTestData("repeater_transform_animated.json");
+ updateProperty(0);
+
+ QVERIFY(qFuzzyCompare(m_transform->position().x(), 50.0));
+}
+
+void tst_BMRepeaterTransform::testAnimatedInitialPositionY()
+{
+ loadTestData("repeater_transform_animated.json");
+ updateProperty(0);
+
+ QVERIFY(qFuzzyCompare(m_transform->position().y(), 50.0));
+}
+
+void tst_BMRepeaterTransform::testAnimatedInitialScaleX()
+{
+ loadTestData("repeater_transform_animated.json");
+ updateProperty(0);
+
+ QVERIFY(qFuzzyCompare(m_transform->scale().x(), 0.5));
+}
+
+void tst_BMRepeaterTransform::testAnimatedInitialScaleY()
+{
+ loadTestData("repeater_transform_animated.json");
+ updateProperty(0);
+
+ QVERIFY(qFuzzyCompare(m_transform->scale().y(), 0.5));
+}
+
+void tst_BMRepeaterTransform::testAnimatedInitialRotation()
+{
+ loadTestData("repeater_transform_animated.json");
+ updateProperty(0);
+
+ QVERIFY(qFuzzyCompare(m_transform->rotation(), 0.0));
+}
+
+void tst_BMRepeaterTransform::testAnimatedInitialStartOpacity()
+{
+ loadTestData("repeater_transform_animated.json");
+ updateProperty(0);
+
+ QVERIFY(qFuzzyCompare(m_transform->startOpacity(), 0.0));
+}
+
+void tst_BMRepeaterTransform::testAnimatedInitialEndOpacity()
+{
+ loadTestData("repeater_transform_animated.json");
+ updateProperty(0);
+
+ QVERIFY(qFuzzyCompare(m_transform->endOpacity(), 100.0));
+}
+
+void tst_BMRepeaterTransform::testAnimatedUpdatedAnchorX()
+{
+ loadTestData("repeater_transform_animated.json");
+ updateProperty(180);
+
+ QVERIFY(qFuzzyCompare(m_transform->anchorPoint().x(), 25.0));
+}
+
+void tst_BMRepeaterTransform::testAnimatedUpdatedAnchorY()
+{
+ loadTestData("repeater_transform_animated.json");
+ updateProperty(180);
+
+ QVERIFY(qFuzzyCompare(m_transform->anchorPoint().y(), 25.0));
+}
+
+void tst_BMRepeaterTransform::testAnimatedUpdatedPositionX()
+{
+ loadTestData("repeater_transform_animated.json");
+ updateProperty(180);
+
+ QVERIFY(qFuzzyCompare(m_transform->position().x(), 75.0));
+}
+
+void tst_BMRepeaterTransform::testAnimatedUpdatedPositionY()
+{
+ loadTestData("repeater_transform_animated.json");
+ updateProperty(180);
+
+ QVERIFY(qFuzzyCompare(m_transform->position().y(), 75.0));
+}
+
+void tst_BMRepeaterTransform::testAnimatedUpdatedScaleX()
+{
+ loadTestData("repeater_transform_animated.json");
+ updateProperty(180);
+
+ QVERIFY(qFuzzyCompare(m_transform->scale().x(), 1.0));
+}
+
+void tst_BMRepeaterTransform::testAnimatedUpdatedScaleY()
+{
+ loadTestData("repeater_transform_animated.json");
+ updateProperty(180);
+
+ QVERIFY(qFuzzyCompare(m_transform->scale().y(), 1.0));
+}
+
+void tst_BMRepeaterTransform::testAnimatedUpdatedRotation()
+{
+ loadTestData("repeater_transform_animated.json");
+ updateProperty(180);
+
+ QVERIFY(qFuzzyCompare(m_transform->rotation(), (3 * 360 + 30.0)));
+}
+
+void tst_BMRepeaterTransform::testAnimatedUpdatedStartOpacity()
+{
+ loadTestData("repeater_transform_animated.json");
+ updateProperty(180);
+
+ QVERIFY(qFuzzyCompare(m_transform->startOpacity(), 25.0));
+}
+
+void tst_BMRepeaterTransform::testAnimatedUpdatedEndOpacity()
+{
+ loadTestData("repeater_transform_animated.json");
+ updateProperty(180);
+
+ QVERIFY(qFuzzyCompare(m_transform->endOpacity(), 75.0));
+}
+
+void tst_BMRepeaterTransform::testActive()
+{
+ loadTestData("repeater_transform_static.json");
+ QVERIFY(m_transform->active(100) == true);
+}
+
+void tst_BMRepeaterTransform::loadTestData(const QByteArray &filename)
+{
+ if (m_transform) {
+ delete m_transform;
+ m_transform = nullptr;
+ }
+
+ QFile sourceFile(QFINDTESTDATA(filename.constData()));
+ if (!sourceFile.exists())
+ QFAIL("File does not exist");
+ if (!sourceFile.open(QIODevice::ReadOnly))
+ QFAIL("Cannot read test file");
+
+ QByteArray json = sourceFile.readAll();
+
+ sourceFile.close();
+
+ QJsonDocument doc = QJsonDocument::fromJson(json);
+ QJsonObject rootObj = doc.object();
+ if (rootObj.empty())
+ QFAIL("Cannot parse test file");
+
+ QJsonArray layers = rootObj.value(QLatin1String("layers")).toArray();
+ QJsonObject layerObj = layers[0].toObject();
+ int type = layerObj.value(QLatin1String("ty")).toInt();
+ if (type != 4)
+ QFAIL("It's not shape layer");
+
+ QJsonArray shapes = layerObj.value(QLatin1String("shapes")).toArray();
+ QJsonArray::const_iterator shapesIt = shapes.constBegin();
+ BMShape* shape = nullptr;
+ while (shapesIt != shapes.end()) {
+ QJsonObject childObj = (*shapesIt).toObject();
+ shape = BMShape::construct(childObj);
+ QVERIFY(shape != nullptr);
+ if (shape->type() == BM_SHAPE_REPEATER_IX)
+ break;
+ shapesIt++;
+ }
+
+ BMRepeater *repeater = static_cast<BMRepeater*>(shape);
+ m_transform = static_cast<BMRepeaterTransform*>(repeater->transform().clone());
+
+ QVERIFY(m_transform != nullptr);
+}
+
+void tst_BMRepeaterTransform::updateProperty(int frame)
+{
+ m_transform->updateProperties(frame);
+}
+
+QTEST_MAIN(tst_BMRepeaterTransform)
+#include "tst_bmrepeatertransform.moc"
diff --git a/tests/auto/bodymovin/shape/shape.pro b/tests/auto/bodymovin/shape/shape.pro
new file mode 100644
index 0000000..dab6f46
--- /dev/null
+++ b/tests/auto/bodymovin/shape/shape.pro
@@ -0,0 +1,13 @@
+TEMPLATE = subdirs
+SUBDIRS += \
+ shapelayer \
+ rect \
+ ellipse \
+ fill \
+ stroke \
+ path \
+ trimpath \
+ transform \
+ shapetransform \
+ repeater \
+ repeatertransform
diff --git a/tests/auto/bodymovin/shape/shapelayer/shape_active_60to120.json b/tests/auto/bodymovin/shape/shapelayer/shape_active_60to120.json
new file mode 100644
index 0000000..8d1da56
--- /dev/null
+++ b/tests/auto/bodymovin/shape/shapelayer/shape_active_60to120.json
@@ -0,0 +1 @@
+{"v":"5.3.4","fr":60,"ip":0,"op":180,"w":100,"h":100,"nm":"EmptyShapeLayer","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[50,50,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[],"ip":60,"op":120,"st":0,"bm":0}],"markers":[]}
diff --git a/tests/auto/bodymovin/shape/shapelayer/shape_mask_alphaclip.json b/tests/auto/bodymovin/shape/shapelayer/shape_mask_alphaclip.json
new file mode 100644
index 0000000..bd913ae
--- /dev/null
+++ b/tests/auto/bodymovin/shape/shapelayer/shape_mask_alphaclip.json
@@ -0,0 +1 @@
+{"v":"5.3.4","fr":60,"ip":0,"op":180,"w":100,"h":100,"nm":"EmptyMaskClipShapeLayer","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 1","td":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[50,50,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[],"ip":60,"op":120,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Shape Layer 2","tt":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[50,50,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[],"ip":-4,"op":180,"st":0,"bm":0}],"markers":[]}
diff --git a/tests/auto/bodymovin/shape/shapelayer/shape_mask_alphainvclip.json b/tests/auto/bodymovin/shape/shapelayer/shape_mask_alphainvclip.json
new file mode 100644
index 0000000..d5854e9
--- /dev/null
+++ b/tests/auto/bodymovin/shape/shapelayer/shape_mask_alphainvclip.json
@@ -0,0 +1 @@
+{"v":"5.3.4","fr":60,"ip":0,"op":180,"w":100,"h":100,"nm":"EmptyMaskClipShapeLayer","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 1","td":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[50,50,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[],"ip":60,"op":120,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Shape Layer 2","tt":2,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[50,50,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[],"ip":-4,"op":180,"st":0,"bm":0}],"markers":[]}
diff --git a/tests/auto/bodymovin/shape/shapelayer/shape_mask_lumaclip.json b/tests/auto/bodymovin/shape/shapelayer/shape_mask_lumaclip.json
new file mode 100644
index 0000000..706fbea
--- /dev/null
+++ b/tests/auto/bodymovin/shape/shapelayer/shape_mask_lumaclip.json
@@ -0,0 +1 @@
+{"v":"5.3.4","fr":60,"ip":0,"op":180,"w":100,"h":100,"nm":"EmptyMaskClipShapeLayer","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 1","td":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[50,50,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[],"ip":60,"op":120,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Shape Layer 2","tt":3,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[50,50,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[],"ip":-4,"op":180,"st":0,"bm":0}],"markers":[]}
diff --git a/tests/auto/bodymovin/shape/shapelayer/shape_mask_lumainvclip.json b/tests/auto/bodymovin/shape/shapelayer/shape_mask_lumainvclip.json
new file mode 100644
index 0000000..a4faa7d
--- /dev/null
+++ b/tests/auto/bodymovin/shape/shapelayer/shape_mask_lumainvclip.json
@@ -0,0 +1 @@
+{"v":"5.3.4","fr":60,"ip":0,"op":180,"w":100,"h":100,"nm":"EmptyMaskClipShapeLayer","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 1","td":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[50,50,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[],"ip":60,"op":120,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Shape Layer 2","tt":4,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[50,50,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[],"ip":-4,"op":180,"st":0,"bm":0}],"markers":[]}
diff --git a/tests/auto/bodymovin/shape/shapelayer/shapelayer.pro b/tests/auto/bodymovin/shape/shapelayer/shapelayer.pro
new file mode 100644
index 0000000..478f5c9
--- /dev/null
+++ b/tests/auto/bodymovin/shape/shapelayer/shapelayer.pro
@@ -0,0 +1,5 @@
+QT += testlib bodymovin-private
+CONFIG += c++11
+
+SOURCES = \
+ tst_bmshapelayer.cpp
diff --git a/tests/auto/bodymovin/shape/shapelayer/tst_bmshapelayer.cpp b/tests/auto/bodymovin/shape/shapelayer/tst_bmshapelayer.cpp
new file mode 100644
index 0000000..0099b41
--- /dev/null
+++ b/tests/auto/bodymovin/shape/shapelayer/tst_bmshapelayer.cpp
@@ -0,0 +1,197 @@
+/****************************************************************************
+**
+** Copyright (C) 2018 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the lottie-qt module of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:GPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 or (at your option) any later version
+** approved by the KDE Free Qt Foundation. The licenses are as published by
+** the Free Software Foundation and appearing in the file LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#include <QtTest/QtTest>
+
+#include <QFile>
+#include <QJsonDocument>
+#include <QJsonObject>
+#include <QJsonArray>
+
+#include "private/bmlayer_p.h"
+#include "private/bmshapelayer_p.h"
+
+class tst_BMShapeLayer: public QObject
+{
+ Q_OBJECT
+
+public:
+ tst_BMShapeLayer();
+ ~tst_BMShapeLayer();
+
+private slots:
+ void initTestCase();
+ void cleanupTestCase();
+
+ void testName();
+ void testType();
+ void testWidth();
+ void testHeight();
+ void testActive();
+ void testIsClippedLayer();
+ void testIsMaskLayer();
+ void testClipMode();
+
+private:
+ void loadTestData(const QByteArray &filename);
+
+ BMShapeLayer *m_layer;
+ BMShapeLayer *m_clippedlayer;
+ qreal m_width;
+ qreal m_height;
+};
+
+tst_BMShapeLayer::tst_BMShapeLayer()
+{
+ m_layer = nullptr;
+ m_clippedlayer = nullptr;
+}
+
+tst_BMShapeLayer::~tst_BMShapeLayer()
+{
+
+}
+
+void tst_BMShapeLayer::initTestCase()
+{
+
+}
+
+void tst_BMShapeLayer::cleanupTestCase()
+{
+
+}
+
+void tst_BMShapeLayer::testName()
+{
+ loadTestData("shape_active_60to120.json");
+ QVERIFY(m_layer->name() == QString("Shape Layer 1"));
+}
+
+void tst_BMShapeLayer::testType()
+{
+ loadTestData("shape_active_60to120.json");
+ QVERIFY(m_layer->type() == BM_LAYER_SHAPE_IX);
+}
+
+void tst_BMShapeLayer::testActive()
+{
+ loadTestData("shape_active_60to120.json");
+ QVERIFY(m_layer->active(0) == false);
+ QVERIFY(m_layer->active(100) == true);
+ QVERIFY(m_layer->active(150) == false);
+}
+
+void tst_BMShapeLayer::testWidth()
+{
+ loadTestData("shape_active_60to120.json");
+ QVERIFY(m_width == 100.0);
+}
+
+void tst_BMShapeLayer::testHeight()
+{
+ loadTestData("shape_active_60to120.json");
+ QVERIFY(m_height == 100.0);
+}
+
+void tst_BMShapeLayer::testIsClippedLayer()
+{
+ loadTestData("shape_active_60to120.json");
+ QVERIFY(m_layer->isClippedLayer() == false);
+ loadTestData("shape_mask_alphaclip.json");
+ QVERIFY(m_clippedlayer->isClippedLayer() == true);
+}
+
+void tst_BMShapeLayer::testIsMaskLayer()
+{
+ loadTestData("shape_active_60to120.json");
+ QVERIFY(m_layer->isMaskLayer() == false);
+ loadTestData("shape_mask_alphaclip.json");
+ QVERIFY(m_layer->isMaskLayer() == true);
+}
+
+void tst_BMShapeLayer::testClipMode()
+{
+ loadTestData("shape_active_60to120.json");
+ QVERIFY(m_layer->clipMode() == BMLayer::MatteClipMode::NoClip);
+ loadTestData("shape_mask_alphaclip.json");
+ QVERIFY(m_clippedlayer->clipMode() == BMLayer::MatteClipMode::Alpha);
+ loadTestData("shape_mask_alphainvclip.json");
+ QVERIFY(m_clippedlayer->clipMode() == BMLayer::MatteClipMode::InvertedAlpha);
+ loadTestData("shape_mask_lumaclip.json");
+ QVERIFY(m_clippedlayer->clipMode() == BMLayer::MatteClipMode::Luminence);
+ loadTestData("shape_mask_lumainvclip.json");
+ QVERIFY(m_clippedlayer->clipMode() == BMLayer::MatteClipMode::InvertedLuminence);
+}
+
+void tst_BMShapeLayer::loadTestData(const QByteArray &filename)
+{
+ if (m_layer) {
+ delete m_layer;
+ m_layer = nullptr;
+ }
+
+ QFile sourceFile(QFINDTESTDATA(filename.constData()));
+ if (!sourceFile.exists())
+ QFAIL("File does not exist");
+ if (!sourceFile.open(QIODevice::ReadOnly))
+ QFAIL("Cannot read test file");
+
+ QByteArray json = sourceFile.readAll();
+
+ sourceFile.close();
+
+ QJsonDocument doc = QJsonDocument::fromJson(json);
+ QJsonObject rootObj = doc.object();
+ if (rootObj.empty())
+ QFAIL("Cannot parse test file");
+
+ m_width = rootObj.value(QLatin1String("w")).toVariant().toReal();
+ m_height = rootObj.value(QLatin1String("h")).toVariant().toReal();
+
+ QJsonArray layers = rootObj.value(QLatin1String("layers")).toArray();
+ QJsonObject layerObj = layers[0].toObject();
+ int type = layerObj.value(QLatin1String("ty")).toInt();
+ if (type != 4)
+ QFAIL("It's not shape layer");
+ m_layer = new BMShapeLayer(layerObj);
+ QVERIFY(m_layer != nullptr);
+
+ if (layers.size() > 1) {
+ layerObj = layers[1].toObject();
+ type = layerObj.value(QLatin1String("ty")).toInt();
+ if (type != 4)
+ QFAIL("it's not shape layer");
+ m_clippedlayer = new BMShapeLayer(layerObj);
+ QVERIFY(m_clippedlayer != nullptr);
+ }
+}
+
+QTEST_MAIN(tst_BMShapeLayer)
+#include "tst_bmshapelayer.moc"
diff --git a/tests/auto/bodymovin/shape/shapetransform/shapetransform.pro b/tests/auto/bodymovin/shape/shapetransform/shapetransform.pro
new file mode 100644
index 0000000..92ae225
--- /dev/null
+++ b/tests/auto/bodymovin/shape/shapetransform/shapetransform.pro
@@ -0,0 +1,5 @@
+QT += testlib gui-private bodymovin-private
+CONFIG += c++11
+
+SOURCES = \
+ tst_bmshapetransform.cpp
diff --git a/tests/auto/bodymovin/shape/shapetransform/shapetransform_animated.json b/tests/auto/bodymovin/shape/shapetransform/shapetransform_animated.json
new file mode 100644
index 0000000..ed650bf
--- /dev/null
+++ b/tests/auto/bodymovin/shape/shapetransform/shapetransform_animated.json
@@ -0,0 +1 @@
+{"v":"5.3.4","fr":60,"ip":0,"op":180,"w":100,"h":100,"nm":"AnimatedShapeTransformShapeLayer","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,0.125,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"tr","p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":0,"s":[50,50],"e":[75,75],"to":[4.16666650772095,4.16666650772095],"ti":[-4.16666650772095,-4.16666650772095]},{"t":179}],"ix":2},"a":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":0,"s":[50,50],"e":[25,25],"to":[-4.16666650772095,-4.16666650772095],"ti":[4.16666650772095,4.16666650772095]},{"t":179}],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"n":["0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167"],"t":0,"s":[50,50],"e":[100,100]},{"t":179}],"ix":3},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":0,"s":[0],"e":[1110]},{"t":179}],"ix":6},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":0,"s":[100],"e":[25]},{"t":179}],"ix":7},"sk":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":0,"s":[0],"e":[4]},{"t":179}],"ix":4},"sa":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":0,"s":[0],"e":[745]},{"t":179}],"ix":5},"nm":"Transform"}],"nm":"Group 1","np":0,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":180,"st":0,"bm":0}],"markers":[]}
diff --git a/tests/auto/bodymovin/shape/shapetransform/shapetransform_static.json b/tests/auto/bodymovin/shape/shapetransform/shapetransform_static.json
new file mode 100644
index 0000000..5d60293
--- /dev/null
+++ b/tests/auto/bodymovin/shape/shapetransform/shapetransform_static.json
@@ -0,0 +1 @@
+{"v":"5.3.4","fr":60,"ip":0,"op":180,"w":100,"h":100,"nm":"StaticShapeTransformShapeLayer","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,0.125,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"tr","p":{"a":0,"k":[50,50],"ix":2},"a":{"a":0,"k":[50,50],"ix":1},"s":{"a":0,"k":[50,50],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":0,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":180,"st":0,"bm":0}],"markers":[]}
diff --git a/tests/auto/bodymovin/shape/shapetransform/tst_bmshapetransform.cpp b/tests/auto/bodymovin/shape/shapetransform/tst_bmshapetransform.cpp
new file mode 100644
index 0000000..4a5e78a
--- /dev/null
+++ b/tests/auto/bodymovin/shape/shapetransform/tst_bmshapetransform.cpp
@@ -0,0 +1,508 @@
+/****************************************************************************
+**
+** Copyright (C) 2018 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the lottie-qt module of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:GPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 or (at your option) any later version
+** approved by the KDE Free Qt Foundation. The licenses are as published by
+** the Free Software Foundation and appearing in the file LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#include <QtTest/QtTest>
+
+#include <QFile>
+#include <QJsonDocument>
+#include <QJsonObject>
+#include <QJsonArray>
+
+#include "private/bmlayer_p.h"
+#include "private/bmgroup_p.h"
+#include "private/bmshapetransform_p.h"
+
+class tst_BMShapeTransform: public QObject
+{
+ Q_OBJECT
+
+public:
+ tst_BMShapeTransform();
+ ~tst_BMShapeTransform();
+
+private:
+
+ // void testParseStaticRect();
+
+private slots:
+ void initTestCase();
+ void cleanupTestCase();
+
+ void testStaticInitialAnchorX();
+ void testStaticInitialAnchorY();
+ void testStaticInitialPositionX();
+ void testStaticInitialPositionY();
+ void testStaticInitialScaleX();
+ void testStaticInitialScaleY();
+ void testStaticInitialRotation();
+ void testStaticInitialOpacity();
+ void testStaticInitialSkew();
+ void testStaticInitialSkewAxis();
+ void testStaticUpdatedAnchorX();
+ void testStaticUpdatedAnchorY();
+ void testStaticUpdatedPositionX();
+ void testStaticUpdatedPositionY();
+ void testStaticUpdatedScaleX();
+ void testStaticUpdatedScaleY();
+ void testStaticUpdatedRotation();
+ void testStaticUpdatedOpacity();
+ void testStaticUpdatedSkew();
+ void testStaticUpdatedSkewAxis();
+
+
+ void testAnimatedInitialAnchorX();
+ void testAnimatedInitialAnchorY();
+ void testAnimatedInitialPositionX();
+ void testAnimatedInitialPositionY();
+ void testAnimatedInitialScaleX();
+ void testAnimatedInitialScaleY();
+ void testAnimatedInitialRotation();
+ void testAnimatedInitialOpacity();
+ void testAnimatedInitialSkew();
+ void testAnimatedInitialSkewAxis();
+ void testAnimatedUpdatedAnchorX();
+ void testAnimatedUpdatedAnchorY();
+ void testAnimatedUpdatedPositionX();
+ void testAnimatedUpdatedPositionY();
+ void testAnimatedUpdatedScaleX();
+ void testAnimatedUpdatedScaleY();
+ void testAnimatedUpdatedRotation();
+ void testAnimatedUpdatedOpacity();
+ void testAnimatedUpdatedSkew();
+ void testAnimatedUpdatedSkewAxis();
+
+ void testName();
+ void testType();
+ void testActive();
+
+private:
+ void loadTestData(const QByteArray &filename);
+ void updateProperty(int frame);
+
+ BMShapeTransform *m_transform = nullptr;
+};
+
+tst_BMShapeTransform::tst_BMShapeTransform()
+{
+
+}
+
+tst_BMShapeTransform::~tst_BMShapeTransform()
+{
+
+}
+
+void tst_BMShapeTransform::initTestCase()
+{
+}
+
+void tst_BMShapeTransform::cleanupTestCase()
+{
+ if (m_transform)
+ delete m_transform;
+}
+
+void tst_BMShapeTransform::testStaticInitialAnchorX()
+{
+ loadTestData("shapetransform_static.json");
+
+ QVERIFY(qFuzzyCompare(m_transform->anchorPoint().x(), 50.0));
+}
+
+void tst_BMShapeTransform::testStaticInitialAnchorY()
+{
+ loadTestData("shapetransform_static.json");
+
+ QVERIFY(qFuzzyCompare(m_transform->anchorPoint().y(), 50.0));
+}
+
+void tst_BMShapeTransform::testStaticInitialPositionX()
+{
+ loadTestData("shapetransform_static.json");
+
+ QVERIFY(qFuzzyCompare(m_transform->position().x(), 50.0));
+}
+
+void tst_BMShapeTransform::testStaticInitialPositionY()
+{
+ loadTestData("shapetransform_static.json");
+
+ QVERIFY(qFuzzyCompare(m_transform->position().y(), 50.0));
+}
+
+void tst_BMShapeTransform::testStaticInitialScaleX()
+{
+ loadTestData("shapetransform_static.json");
+
+ QVERIFY(qFuzzyCompare(m_transform->scale().x(), 0.5));
+}
+
+void tst_BMShapeTransform::testStaticInitialScaleY()
+{
+ loadTestData("shapetransform_static.json");
+
+ QVERIFY(qFuzzyCompare(m_transform->scale().y(), 0.5));
+}
+
+void tst_BMShapeTransform::testStaticInitialRotation()
+{
+ loadTestData("shapetransform_static.json");
+
+ QVERIFY(qFuzzyCompare(m_transform->rotation(), 0.0));
+}
+
+void tst_BMShapeTransform::testStaticInitialOpacity()
+{
+ loadTestData("shapetransform_static.json");
+
+ QVERIFY(qFuzzyCompare(m_transform->opacity(), 1.0));
+}
+
+void tst_BMShapeTransform::testStaticInitialSkew()
+{
+ loadTestData("shapetransform_static.json");
+
+ QVERIFY(qFuzzyCompare(m_transform->skew(), 0.0));
+}
+
+void tst_BMShapeTransform::testStaticInitialSkewAxis()
+{
+ loadTestData("shapetransform_static.json");
+
+ QVERIFY(qFuzzyCompare(m_transform->skewAxis(), 0.0));
+}
+
+void tst_BMShapeTransform::testStaticUpdatedAnchorX()
+{
+ loadTestData("shapetransform_static.json");
+ updateProperty(180);
+
+ QVERIFY(qFuzzyCompare(m_transform->anchorPoint().x(), 50.0));
+}
+
+void tst_BMShapeTransform::testStaticUpdatedAnchorY()
+{
+ loadTestData("shapetransform_static.json");
+ updateProperty(180);
+
+ QVERIFY(qFuzzyCompare(m_transform->anchorPoint().y(), 50.0));
+}
+
+void tst_BMShapeTransform::testStaticUpdatedPositionX()
+{
+ loadTestData("shapetransform_static.json");
+ updateProperty(180);
+
+ QVERIFY(qFuzzyCompare(m_transform->position().x(), 50.0));
+}
+
+void tst_BMShapeTransform::testStaticUpdatedPositionY()
+{
+ loadTestData("shapetransform_static.json");
+ updateProperty(180);
+
+ QVERIFY(qFuzzyCompare(m_transform->position().y(), 50.0));
+}
+
+void tst_BMShapeTransform::testStaticUpdatedScaleX()
+{
+ loadTestData("shapetransform_static.json");
+ updateProperty(180);
+
+ QVERIFY(qFuzzyCompare(m_transform->scale().x(), 0.5));
+}
+
+void tst_BMShapeTransform::testStaticUpdatedScaleY()
+{
+ loadTestData("shapetransform_static.json");
+ updateProperty(180);
+
+ QVERIFY(qFuzzyCompare(m_transform->scale().y(), 0.5));
+}
+
+void tst_BMShapeTransform::testStaticUpdatedRotation()
+{
+ loadTestData("shapetransform_static.json");
+ updateProperty(180);
+
+ QVERIFY(qFuzzyCompare(m_transform->rotation(), 0.0));
+}
+
+void tst_BMShapeTransform::testStaticUpdatedOpacity()
+{
+ loadTestData("shapetransform_static.json");
+ updateProperty(180);
+
+ QVERIFY(qFuzzyCompare(m_transform->opacity(), 1.0));
+}
+
+void tst_BMShapeTransform::testStaticUpdatedSkew()
+{
+ loadTestData("shapetransform_static.json");
+ updateProperty(180);
+
+ QVERIFY(qFuzzyCompare(m_transform->skew(), 0.0));
+}
+
+void tst_BMShapeTransform::testStaticUpdatedSkewAxis()
+{
+ loadTestData("shapetransform_static.json");
+ updateProperty(180);
+
+ QVERIFY(qFuzzyCompare(m_transform->skewAxis(), 0.0));
+}
+
+void tst_BMShapeTransform::testAnimatedInitialAnchorX()
+{
+ loadTestData("shapetransform_animated.json");
+ updateProperty(0);
+
+ QVERIFY(qFuzzyCompare(m_transform->anchorPoint().x(), 50.0));
+}
+
+void tst_BMShapeTransform::testAnimatedInitialAnchorY()
+{
+ loadTestData("shapetransform_animated.json");
+ updateProperty(0);
+
+ QVERIFY(qFuzzyCompare(m_transform->anchorPoint().y(), 50.0));
+}
+
+void tst_BMShapeTransform::testAnimatedInitialPositionX()
+{
+ loadTestData("shapetransform_animated.json");
+ updateProperty(0);
+
+ QVERIFY(qFuzzyCompare(m_transform->position().x(), 50.0));
+}
+
+void tst_BMShapeTransform::testAnimatedInitialPositionY()
+{
+ loadTestData("shapetransform_animated.json");
+ updateProperty(0);
+
+ QVERIFY(qFuzzyCompare(m_transform->position().y(), 50.0));
+}
+
+void tst_BMShapeTransform::testAnimatedInitialScaleX()
+{
+ loadTestData("shapetransform_animated.json");
+ updateProperty(0);
+
+ QVERIFY(qFuzzyCompare(m_transform->scale().x(), 0.5));
+}
+
+void tst_BMShapeTransform::testAnimatedInitialScaleY()
+{
+ loadTestData("shapetransform_animated.json");
+ updateProperty(0);
+
+ QVERIFY(qFuzzyCompare(m_transform->scale().y(), 0.5));
+}
+
+void tst_BMShapeTransform::testAnimatedInitialRotation()
+{
+ loadTestData("shapetransform_animated.json");
+ updateProperty(0);
+
+ QVERIFY(qFuzzyCompare(m_transform->rotation(), 0.0));
+}
+
+void tst_BMShapeTransform::testAnimatedInitialOpacity()
+{
+ loadTestData("shapetransform_animated.json");
+ updateProperty(0);
+
+ QVERIFY(qFuzzyCompare(m_transform->opacity(), 1.0));
+}
+
+void tst_BMShapeTransform::testAnimatedInitialSkew()
+{
+ loadTestData("shapetransform_animated.json");
+ updateProperty(0);
+
+ QVERIFY(qFuzzyCompare(m_transform->skew(), 0.0));
+}
+
+void tst_BMShapeTransform::testAnimatedInitialSkewAxis()
+{
+ loadTestData("shapetransform_animated.json");
+ updateProperty(0);
+
+ QVERIFY(qFuzzyCompare(m_transform->skewAxis(), 0.0));
+}
+
+void tst_BMShapeTransform::testAnimatedUpdatedAnchorX()
+{
+ loadTestData("shapetransform_animated.json");
+ updateProperty(180);
+
+ QVERIFY(qFuzzyCompare(m_transform->anchorPoint().x(), 25.0));
+}
+
+void tst_BMShapeTransform::testAnimatedUpdatedAnchorY()
+{
+ loadTestData("shapetransform_animated.json");
+ updateProperty(180);
+
+ QVERIFY(qFuzzyCompare(m_transform->anchorPoint().y(), 25.0));
+}
+
+void tst_BMShapeTransform::testAnimatedUpdatedPositionX()
+{
+ loadTestData("shapetransform_animated.json");
+ updateProperty(180);
+
+ QVERIFY(qFuzzyCompare(m_transform->position().x(), 75.0));
+}
+
+void tst_BMShapeTransform::testAnimatedUpdatedPositionY()
+{
+ loadTestData("shapetransform_animated.json");
+ updateProperty(180);
+
+ QVERIFY(qFuzzyCompare(m_transform->position().y(), 75.0));
+}
+
+void tst_BMShapeTransform::testAnimatedUpdatedScaleX()
+{
+ loadTestData("shapetransform_animated.json");
+ updateProperty(180);
+
+ QVERIFY(qFuzzyCompare(m_transform->scale().x(), 1.0));
+}
+
+void tst_BMShapeTransform::testAnimatedUpdatedScaleY()
+{
+ loadTestData("shapetransform_animated.json");
+ updateProperty(180);
+
+ QVERIFY(qFuzzyCompare(m_transform->scale().y(), 1.0));
+}
+
+void tst_BMShapeTransform::testAnimatedUpdatedRotation()
+{
+ loadTestData("shapetransform_animated.json");
+ updateProperty(180);
+
+ QVERIFY(qFuzzyCompare(m_transform->rotation(), (3 * 360 + 30.0)));
+}
+
+void tst_BMShapeTransform::testAnimatedUpdatedOpacity()
+{
+ loadTestData("shapetransform_animated.json");
+ updateProperty(180);
+
+ QVERIFY(qFuzzyCompare(m_transform->opacity(), 0.25));
+}
+
+void tst_BMShapeTransform::testAnimatedUpdatedSkew()
+{
+ loadTestData("shapetransform_animated.json");
+ updateProperty(180);
+
+ QVERIFY(qFuzzyCompare(m_transform->skew(), 4.0));
+}
+
+void tst_BMShapeTransform::testAnimatedUpdatedSkewAxis()
+{
+ loadTestData("shapetransform_animated.json");
+ updateProperty(180);
+
+ QVERIFY(qFuzzyCompare(m_transform->skewAxis(), (2 * 360 + 25.0)));
+}
+
+void tst_BMShapeTransform::testName()
+{
+ loadTestData("shapetransform_static.json");
+ QVERIFY(m_transform->name() == QString("Transform"));
+}
+
+void tst_BMShapeTransform::testType()
+{
+ loadTestData("shapetransform_static.json");
+ QVERIFY(m_transform->type() == BM_SHAPE_TRANS_IX);
+}
+
+void tst_BMShapeTransform::testActive()
+{
+ loadTestData("shapetransform_static.json");
+ QVERIFY(m_transform->active(100) == true);
+}
+
+void tst_BMShapeTransform::loadTestData(const QByteArray &filename)
+{
+ if (m_transform) {
+ delete m_transform;
+ m_transform = nullptr;
+ }
+
+ QFile sourceFile(QFINDTESTDATA(filename.constData()));
+ if (!sourceFile.exists())
+ QFAIL("File does not exist");
+ if (!sourceFile.open(QIODevice::ReadOnly))
+ QFAIL("Cannot read test file");
+
+ QByteArray json = sourceFile.readAll();
+
+ sourceFile.close();
+
+ QJsonDocument doc = QJsonDocument::fromJson(json);
+ QJsonObject rootObj = doc.object();
+ if (rootObj.empty())
+ QFAIL("Cannot parse test file");
+
+ QJsonArray layers = rootObj.value(QLatin1String("layers")).toArray();
+ QJsonObject layerObj = layers[0].toObject();
+ int type = layerObj.value(QLatin1String("ty")).toInt();
+ if (type != 4)
+ QFAIL("It's not shape layer");
+
+ QJsonArray shapes = layerObj.value(QLatin1String("shapes")).toArray();
+ QJsonArray::const_iterator shapesIt = shapes.constBegin();
+ BMGroup* group = nullptr;
+ while (shapesIt != shapes.end()) {
+ QJsonObject childObj = (*shapesIt).toObject();
+ QByteArray type = childObj.value(QLatin1String("ty")).toVariant().toByteArray();
+ if (QLatin1String(type.data()) == QLatin1String(BM_SHAPE_GROUP_STR))
+ group = new BMGroup(childObj);
+ shapesIt++;
+ }
+ m_transform = static_cast<BMShapeTransform*>(group->findChild("Transform"));
+
+ QVERIFY(m_transform != nullptr);
+}
+
+void tst_BMShapeTransform::updateProperty(int frame)
+{
+ m_transform->updateProperties(frame);
+}
+
+QTEST_MAIN(tst_BMShapeTransform)
+#include "tst_bmshapetransform.moc"
diff --git a/tests/auto/bodymovin/shape/stroke/stroke.pro b/tests/auto/bodymovin/shape/stroke/stroke.pro
new file mode 100644
index 0000000..ce75670
--- /dev/null
+++ b/tests/auto/bodymovin/shape/stroke/stroke.pro
@@ -0,0 +1,5 @@
+QT += testlib gui-private bodymovin-private
+CONFIG += c++11
+
+SOURCES = \
+ tst_bmstroke.cpp
diff --git a/tests/auto/bodymovin/shape/stroke/stroke_animated_blue5_white1.json b/tests/auto/bodymovin/shape/stroke/stroke_animated_blue5_white1.json
new file mode 100644
index 0000000..b9f3445
--- /dev/null
+++ b/tests/auto/bodymovin/shape/stroke/stroke_animated_blue5_white1.json
@@ -0,0 +1,213 @@
+{
+ "assets" : [],
+ "ddd" : 0,
+ "fr" : 60,
+ "h" : 100,
+ "ip" : 0,
+ "layers" : [
+ {
+ "ao" : 0,
+ "bm" : 0,
+ "ddd" : 0,
+ "ind" : 1,
+ "ip" : 0,
+ "ks" : {
+ "a" : {
+ "a" : 0,
+ "ix" : 1,
+ "k" : [ 0, 0, 0 ]
+ },
+ "o" : {
+ "a" : 0,
+ "ix" : 11,
+ "k" : 100
+ },
+ "p" : {
+ "a" : 0,
+ "ix" : 2,
+ "k" : [ 0, 0, 0 ]
+ },
+ "r" : {
+ "a" : 0,
+ "ix" : 10,
+ "k" : 0
+ },
+ "s" : {
+ "a" : 0,
+ "ix" : 6,
+ "k" : [ 100, 100, 100 ]
+ }
+ },
+ "nm" : "Shape Layer 1",
+ "op" : 180,
+ "shapes" : [
+ {
+ "c" : {
+ "a" : 1,
+ "ix" : 3,
+ "k" : [
+ {
+ "e" : [ 1, 1, 1, 1 ],
+ "i" : {
+ "x" : [ 0.8330 ],
+ "y" : [ 0.8330 ]
+ },
+ "n" : [ "0p833_0p833_0p167_0p167" ],
+ "o" : {
+ "x" : [ 0.1670 ],
+ "y" : [ 0.1670 ]
+ },
+ "s" : [ 0, 0, 1, 1 ],
+ "t" : 0
+ },
+ {
+ "t" : 179
+ }
+ ]
+ },
+ "d" : [
+ {
+ "n" : "d",
+ "nm" : "dash",
+ "v" : {
+ "a" : 1,
+ "ix" : 1,
+ "k" : [
+ {
+ "e" : [ 30 ],
+ "i" : {
+ "x" : [ 0.8330 ],
+ "y" : [ 0.8330 ]
+ },
+ "n" : [ "0p833_0p833_0p167_0p167" ],
+ "o" : {
+ "x" : [ 0.1670 ],
+ "y" : [ 0.1670 ]
+ },
+ "s" : [ 10 ],
+ "t" : 0
+ },
+ {
+ "t" : 179
+ }
+ ]
+ }
+ },
+ {
+ "n" : "o",
+ "nm" : "offset",
+ "v" : {
+ "a" : 1,
+ "ix" : 7,
+ "k" : [
+ {
+ "e" : [ 10 ],
+ "i" : {
+ "x" : [ 0.8330 ],
+ "y" : [ 0.8330 ]
+ },
+ "n" : [ "0p833_0p833_0p167_0p167" ],
+ "o" : {
+ "x" : [ 0.1670 ],
+ "y" : [ 0.1670 ]
+ },
+ "s" : [ 0 ],
+ "t" : 0
+ },
+ {
+ "t" : 179
+ }
+ ]
+ }
+ }
+ ],
+ "hd" : false,
+ "lc" : 1,
+ "lj" : 1,
+ "ml" : 4,
+ "ml2" : {
+ "a" : 1,
+ "ix" : 8,
+ "k" : [
+ {
+ "e" : [ 8 ],
+ "i" : {
+ "x" : [ 0.8330 ],
+ "y" : [ 0.8330 ]
+ },
+ "n" : [ "0p833_0p833_0p167_0p167" ],
+ "o" : {
+ "x" : [ 0.1670 ],
+ "y" : [ 0.1670 ]
+ },
+ "s" : [ 4 ],
+ "t" : 0
+ },
+ {
+ "t" : 179
+ }
+ ]
+ },
+ "mn" : "ADBE Vector Graphic - Stroke",
+ "nm" : "Stroke 1",
+ "o" : {
+ "a" : 1,
+ "ix" : 4,
+ "k" : [
+ {
+ "e" : [ 10 ],
+ "i" : {
+ "x" : [ 0.8330 ],
+ "y" : [ 0.8330 ]
+ },
+ "n" : [ "0p833_0p833_0p167_0p167" ],
+ "o" : {
+ "x" : [ 0.1670 ],
+ "y" : [ 0.1670 ]
+ },
+ "s" : [ 100 ],
+ "t" : 0
+ },
+ {
+ "t" : 179
+ }
+ ]
+ },
+ "ty" : "st",
+ "w" : {
+ "a" : 1,
+ "ix" : 5,
+ "k" : [
+ {
+ "e" : [ 1 ],
+ "i" : {
+ "x" : [ 0.8330 ],
+ "y" : [ 0.8330 ]
+ },
+ "n" : [ "0p833_0p833_0p167_0p167" ],
+ "o" : {
+ "x" : [ 0.1670 ],
+ "y" : [ 0.1670 ]
+ },
+ "s" : [ 5 ],
+ "t" : 0
+ },
+ {
+ "t" : 179
+ }
+ ]
+ }
+ }
+ ],
+ "sr" : 1,
+ "st" : 0,
+ "ty" : 4
+ }
+ ],
+ "markers" : [],
+ "nm" : "AnimatedStrokeShapeLayer",
+ "op" : 180,
+ "v" : "5.3.4",
+ "w" : 100
+}
+
diff --git a/tests/auto/bodymovin/shape/stroke/stroke_hidden.json b/tests/auto/bodymovin/shape/stroke/stroke_hidden.json
new file mode 100644
index 0000000..d49699d
--- /dev/null
+++ b/tests/auto/bodymovin/shape/stroke/stroke_hidden.json
@@ -0,0 +1 @@
+{"v":"5.3.4","fr":60,"ip":0,"op":180,"w":100,"h":100,"nm":"StaticStrokeShapeLayer","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,0,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"st","c":{"a":0,"k":[0,0,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":1,"lj":1,"ml":4,"ml2":{"a":0,"k":4,"ix":8},"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":true}],"ip":0,"op":180,"st":0,"bm":0}],"markers":[]}
diff --git a/tests/auto/bodymovin/shape/stroke/stroke_static_blue_2.json b/tests/auto/bodymovin/shape/stroke/stroke_static_blue_2.json
new file mode 100644
index 0000000..f8ff748
--- /dev/null
+++ b/tests/auto/bodymovin/shape/stroke/stroke_static_blue_2.json
@@ -0,0 +1 @@
+{"v":"5.3.4","fr":60,"ip":0,"op":180,"w":100,"h":100,"nm":"StaticStrokeShapeLayer","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,0,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"st","c":{"a":0,"k":[0,0,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":1,"lj":1,"ml":4,"ml2":{"a":0,"k":4,"ix":8},"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false}],"ip":0,"op":180,"st":0,"bm":0}],"markers":[]}
diff --git a/tests/auto/bodymovin/shape/stroke/tst_bmstroke.cpp b/tests/auto/bodymovin/shape/stroke/tst_bmstroke.cpp
new file mode 100644
index 0000000..c32284a
--- /dev/null
+++ b/tests/auto/bodymovin/shape/stroke/tst_bmstroke.cpp
@@ -0,0 +1,349 @@
+/****************************************************************************
+**
+** Copyright (C) 2018 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the lottie-qt module of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:GPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 or (at your option) any later version
+** approved by the KDE Free Qt Foundation. The licenses are as published by
+** the Free Software Foundation and appearing in the file LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#include <QtTest/QtTest>
+
+#include <QFile>
+#include <QJsonDocument>
+#include <QJsonObject>
+#include <QJsonArray>
+
+#include "private/bmlayer_p.h"
+#include "private/bmstroke_p.h"
+
+class tst_BMStroke: public QObject
+{
+ Q_OBJECT
+
+public:
+ tst_BMStroke();
+ ~tst_BMStroke();
+
+private:
+
+private slots:
+ void initTestCase();
+ void cleanupTestCase();
+
+ void testStaticInitialColor();
+ void testStaticInitialOpacity();
+ void testStaticInitialWidth();
+ void testStaticInitialCapStyle();
+ void testStaticInitialJoinStyle();
+ void testStaticInitialMiterLimit();
+ void testStaticUpdatedColor();
+ void testStaticUpdatedOpacity();
+ void testStaticUpdatedWidth();
+ void testStaticUpdatedCapStyle();
+ void testStaticUpdatedJoinStyle();
+ void testStaticUpdatedMiterLimit();
+
+ void testAnimatedInitialColor();
+ void testAnimatedInitialOpacity();
+ void testAnimatedInitialWidth();
+ void testAnimatedInitialCapStyle();
+ void testAnimatedInitialJoinStyle();
+ void testAnimatedInitialMiterLimit();
+ void testAnimatedUpdatedColor();
+ void testAnimatedUpdatedOpacity();
+ void testAnimatedUpdatedWidth();
+
+ void testName();
+ void testType();
+ void testActive();
+ void testHidden();
+
+private:
+ void loadTestData(const QByteArray &filename);
+ void updateProperty(int frame);
+
+ BMStroke *m_stroke = nullptr;
+};
+
+tst_BMStroke::tst_BMStroke()
+{
+
+}
+
+tst_BMStroke::~tst_BMStroke()
+{
+
+}
+
+void tst_BMStroke::initTestCase()
+{
+}
+
+void tst_BMStroke::cleanupTestCase()
+{
+ if (m_stroke)
+ delete m_stroke;
+}
+
+void tst_BMStroke::testStaticInitialColor()
+{
+ loadTestData("stroke_static_blue_2.json");
+
+ QVERIFY(m_stroke->pen().color() == QColor(Qt::blue));
+}
+
+void tst_BMStroke::testStaticInitialOpacity()
+{
+ loadTestData("stroke_static_blue_2.json");
+
+ QVERIFY(qFuzzyCompare(m_stroke->opacity(), 100.0));
+}
+
+void tst_BMStroke::testStaticInitialWidth()
+{
+ loadTestData("stroke_static_blue_2.json");
+
+ QVERIFY(qFuzzyCompare(m_stroke->pen().width(), 2.0));
+}
+
+void tst_BMStroke::testStaticInitialCapStyle()
+{
+ loadTestData("stroke_static_blue_2.json");
+
+ QVERIFY(m_stroke->pen().capStyle() == Qt::FlatCap);
+}
+
+void tst_BMStroke::testStaticInitialJoinStyle()
+{
+ loadTestData("stroke_static_blue_2.json");
+
+ QVERIFY(m_stroke->pen().joinStyle() == Qt::MiterJoin);
+}
+
+void tst_BMStroke::testStaticInitialMiterLimit()
+{
+ loadTestData("stroke_static_blue_2.json");
+
+ QVERIFY(qFuzzyCompare(m_stroke->pen().miterLimit(), 4.0));
+}
+
+void tst_BMStroke::testStaticUpdatedColor()
+{
+ loadTestData("stroke_static_blue_2.json");
+ updateProperty(179);
+
+ QVERIFY(m_stroke->pen().color() == QColor(Qt::blue));
+}
+
+void tst_BMStroke::testStaticUpdatedOpacity()
+{
+ loadTestData("stroke_static_blue_2.json");
+ updateProperty(179);
+
+ QVERIFY(qFuzzyCompare(m_stroke->opacity(), 100.0));
+}
+
+void tst_BMStroke::testStaticUpdatedWidth()
+{
+ loadTestData("stroke_static_blue_2.json");
+ updateProperty(179);
+
+ QVERIFY(qFuzzyCompare(m_stroke->pen().width(), 2.0));
+}
+
+void tst_BMStroke::testStaticUpdatedCapStyle()
+{
+ loadTestData("stroke_static_blue_2.json");
+ updateProperty(179);
+
+ QVERIFY(m_stroke->pen().capStyle() == Qt::FlatCap);
+}
+
+void tst_BMStroke::testStaticUpdatedJoinStyle()
+{
+ loadTestData("stroke_static_blue_2.json");
+ updateProperty(179);
+
+ QVERIFY(m_stroke->pen().joinStyle() == Qt::MiterJoin);
+}
+
+void tst_BMStroke::testStaticUpdatedMiterLimit()
+{
+ loadTestData("stroke_static_blue_2.json");
+ updateProperty(179);
+
+ QVERIFY(qFuzzyCompare(m_stroke->pen().miterLimit(), 4.0));
+}
+
+void tst_BMStroke::testAnimatedInitialColor()
+{
+ loadTestData("stroke_animated_blue5_white1.json");
+ updateProperty(0);
+
+ QVERIFY(m_stroke->pen().color() == QColor(Qt::blue));
+}
+
+void tst_BMStroke::testAnimatedInitialOpacity()
+{
+ loadTestData("stroke_animated_blue5_white1.json");
+ updateProperty(0);
+
+ QVERIFY(qFuzzyCompare(m_stroke->opacity(), 100.0));
+}
+
+void tst_BMStroke::testAnimatedInitialWidth()
+{
+ loadTestData("stroke_animated_blue5_white1.json");
+ updateProperty(0);
+
+ QVERIFY(qFuzzyCompare(m_stroke->pen().width(), 5.0));
+}
+
+void tst_BMStroke::testAnimatedInitialCapStyle()
+{
+ loadTestData("stroke_animated_blue5_white1.json");
+ updateProperty(0);
+
+ QVERIFY(m_stroke->pen().capStyle() == Qt::FlatCap);
+}
+
+void tst_BMStroke::testAnimatedInitialJoinStyle()
+{
+ loadTestData("stroke_animated_blue5_white1.json");
+ updateProperty(0);
+
+ QVERIFY(m_stroke->pen().joinStyle() == Qt::MiterJoin);
+}
+
+void tst_BMStroke::testAnimatedInitialMiterLimit()
+{
+ loadTestData("stroke_animated_blue5_white1.json");
+ updateProperty(0);
+
+ QVERIFY(qFuzzyCompare(m_stroke->pen().miterLimit(), 4.0));
+}
+
+void tst_BMStroke::testAnimatedUpdatedColor()
+{
+ loadTestData("stroke_animated_blue5_white1.json");
+ updateProperty(179);
+
+ QVERIFY(m_stroke->pen().color() == QColor(Qt::white));
+}
+
+void tst_BMStroke::testAnimatedUpdatedOpacity()
+{
+ loadTestData("stroke_animated_blue5_white1.json");
+ updateProperty(179);
+
+ QVERIFY(qFuzzyCompare(m_stroke->opacity(), 10.0));
+}
+
+void tst_BMStroke::testAnimatedUpdatedWidth()
+{
+ loadTestData("stroke_animated_blue5_white1.json");
+ updateProperty(179);
+
+ QVERIFY(qFuzzyCompare(m_stroke->pen().width(), 1.0));
+}
+
+void tst_BMStroke::testName()
+{
+ loadTestData("stroke_static_blue_2.json");
+ QVERIFY(m_stroke->name() == QString("Stroke 1"));
+}
+
+void tst_BMStroke::testType()
+{
+ loadTestData("stroke_static_blue_2.json");
+ QVERIFY(m_stroke->type() == BM_SHAPE_STROKE_IX);
+}
+
+void tst_BMStroke::testActive()
+{
+ loadTestData("stroke_static_blue_2.json");
+ QVERIFY(m_stroke->active(100) == true);
+
+ loadTestData("stroke_hidden.json");
+ QVERIFY(m_stroke->active(100) == false);
+}
+
+void tst_BMStroke::testHidden()
+{
+ loadTestData("stroke_hidden.json");
+ QVERIFY(m_stroke->hidden() == true);
+}
+
+void tst_BMStroke::loadTestData(const QByteArray &filename)
+{
+ if (m_stroke) {
+ delete m_stroke;
+ m_stroke = nullptr;
+ }
+
+ QFile sourceFile(QFINDTESTDATA(filename.constData()));
+ if (!sourceFile.exists())
+ QFAIL("File does not exist");
+ if (!sourceFile.open(QIODevice::ReadOnly))
+ QFAIL("Cannot read test file");
+
+ QByteArray json = sourceFile.readAll();
+
+ sourceFile.close();
+
+ QJsonDocument doc = QJsonDocument::fromJson(json);
+ QJsonObject rootObj = doc.object();
+ if (rootObj.empty())
+ QFAIL("Cannot parse test file");
+
+ QJsonArray layers = rootObj.value(QLatin1String("layers")).toArray();
+ QJsonObject layerObj = layers[0].toObject();
+ int type = layerObj.value(QLatin1String("ty")).toInt();
+ if (type != 4)
+ QFAIL("It's not shape layer");
+
+ QJsonArray shapes = layerObj.value(QLatin1String("shapes")).toArray();
+ QJsonArray::const_iterator shapesIt = shapes.constBegin();
+ BMShape* shape = nullptr;
+ while (shapesIt != shapes.end()) {
+ QJsonObject childObj = (*shapesIt).toObject();
+ shape = BMShape::construct(childObj);
+ QVERIFY(shape != nullptr);
+ if (shape->type() == BM_SHAPE_STROKE_IX)
+ break;
+ shapesIt++;
+ }
+
+ m_stroke = static_cast<BMStroke*>(shape);
+
+ QVERIFY(m_stroke != nullptr);
+}
+
+void tst_BMStroke::updateProperty(int frame)
+{
+ m_stroke->updateProperties(frame);
+}
+
+QTEST_MAIN(tst_BMStroke)
+#include "tst_bmstroke.moc"
diff --git a/tests/auto/bodymovin/shape/transform/transform.pro b/tests/auto/bodymovin/shape/transform/transform.pro
new file mode 100644
index 0000000..cdb0a83
--- /dev/null
+++ b/tests/auto/bodymovin/shape/transform/transform.pro
@@ -0,0 +1,6 @@
+QT += testlib gui-private bodymovin-private
+CONFIG += c++11
+
+SOURCES = \
+ tst_bmbasictransform.cpp
+
diff --git a/tests/auto/bodymovin/shape/transform/transform_animated.json b/tests/auto/bodymovin/shape/transform/transform_animated.json
new file mode 100644
index 0000000..c06f5e3
--- /dev/null
+++ b/tests/auto/bodymovin/shape/transform/transform_animated.json
@@ -0,0 +1 @@
+{"v":"5.3.4","fr":60,"ip":0,"op":180,"w":100,"h":100,"nm":"StaticTransformShapeLayer","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":0,"s":[100],"e":[25]},{"t":179}],"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":0,"s":[0],"e":[1110]},{"t":179}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":0,"s":[50,50,0],"e":[75,75,0],"to":[4.16666650772095,4.16666650772095,0],"ti":[-4.16666650772095,-4.16666650772095,0]},{"t":179}],"ix":2},"a":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":0,"s":[50,50,0],"e":[25,25,0],"to":[-4.16666650772095,-4.16666650772095,0],"ti":[4.16666650772095,4.16666650772095,0]},{"t":179}],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"n":["0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167"],"t":0,"s":[50,50,100],"e":[100,100,100]},{"t":179}],"ix":6}},"ao":0,"shapes":[],"ip":-179,"op":184,"st":-179,"bm":0}],"markers":[]}
diff --git a/tests/auto/bodymovin/shape/transform/transform_static.json b/tests/auto/bodymovin/shape/transform/transform_static.json
new file mode 100644
index 0000000..d1d7edf
--- /dev/null
+++ b/tests/auto/bodymovin/shape/transform/transform_static.json
@@ -0,0 +1 @@
+{"v":"5.3.4","fr":60,"ip":0,"op":180,"w":100,"h":100,"nm":"AnimatedTransformShapeLayer","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[50,50,0],"ix":2},"a":{"a":0,"k":[50,50,0],"ix":1},"s":{"a":0,"k":[50,50,100],"ix":6}},"ao":0,"shapes":[],"ip":-179,"op":184,"st":-179,"bm":0}],"markers":[]}
diff --git a/tests/auto/bodymovin/shape/transform/tst_bmbasictransform.cpp b/tests/auto/bodymovin/shape/transform/tst_bmbasictransform.cpp
new file mode 100644
index 0000000..d29501c
--- /dev/null
+++ b/tests/auto/bodymovin/shape/transform/tst_bmbasictransform.cpp
@@ -0,0 +1,413 @@
+/****************************************************************************
+**
+** Copyright (C) 2018 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the lottie-qt module of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:GPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 or (at your option) any later version
+** approved by the KDE Free Qt Foundation. The licenses are as published by
+** the Free Software Foundation and appearing in the file LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#include <QtTest/QtTest>
+
+#include <QFile>
+#include <QJsonDocument>
+#include <QJsonObject>
+#include <QJsonArray>
+
+#include "private/bmlayer_p.h"
+#include "private/bmbasictransform_p.h"
+
+class tst_BMBasicTransform: public QObject
+{
+ Q_OBJECT
+
+public:
+ tst_BMBasicTransform();
+ ~tst_BMBasicTransform();
+
+private:
+
+ // void testParseStaticRect();
+
+private slots:
+ void initTestCase();
+ void cleanupTestCase();
+
+ void testStaticInitialAnchorX();
+ void testStaticInitialAnchorY();
+ void testStaticInitialPositionX();
+ void testStaticInitialPositionY();
+ void testStaticInitialScaleX();
+ void testStaticInitialScaleY();
+ void testStaticInitialRotation();
+ void testStaticInitialOpacity();
+ void testStaticUpdatedAnchorX();
+ void testStaticUpdatedAnchorY();
+ void testStaticUpdatedPositionX();
+ void testStaticUpdatedPositionY();
+ void testStaticUpdatedScaleX();
+ void testStaticUpdatedScaleY();
+ void testStaticUpdatedRotation();
+ void testStaticUpdatedOpacity();
+
+ void testAnimatedInitialAnchorX();
+ void testAnimatedInitialAnchorY();
+ void testAnimatedInitialPositionX();
+ void testAnimatedInitialPositionY();
+ void testAnimatedInitialScaleX();
+ void testAnimatedInitialScaleY();
+ void testAnimatedInitialRotation();
+ void testAnimatedInitialOpacity();
+ void testAnimatedUpdatedAnchorX();
+ void testAnimatedUpdatedAnchorY();
+ void testAnimatedUpdatedPositionX();
+ void testAnimatedUpdatedPositionY();
+ void testAnimatedUpdatedScaleX();
+ void testAnimatedUpdatedScaleY();
+ void testAnimatedUpdatedRotation();
+ void testAnimatedUpdatedOpacity();
+
+ void testActive();
+
+private:
+ void loadTestData(const QByteArray &filename);
+ void updateProperty(int frame);
+
+ BMBasicTransform *m_transform = nullptr;
+};
+
+tst_BMBasicTransform::tst_BMBasicTransform()
+{
+
+}
+
+tst_BMBasicTransform::~tst_BMBasicTransform()
+{
+
+}
+
+void tst_BMBasicTransform::initTestCase()
+{
+}
+
+void tst_BMBasicTransform::cleanupTestCase()
+{
+ if (m_transform)
+ delete m_transform;
+}
+
+void tst_BMBasicTransform::testStaticInitialAnchorX()
+{
+ loadTestData("transform_static.json");
+
+ QVERIFY(qFuzzyCompare(m_transform->anchorPoint().x(), 50.0));
+}
+
+void tst_BMBasicTransform::testStaticInitialAnchorY()
+{
+ loadTestData("transform_static.json");
+
+ QVERIFY(qFuzzyCompare(m_transform->anchorPoint().y(), 50.0));
+}
+
+void tst_BMBasicTransform::testStaticInitialPositionX()
+{
+ loadTestData("transform_static.json");
+
+ QVERIFY(qFuzzyCompare(m_transform->position().x(), 50.0));
+}
+
+void tst_BMBasicTransform::testStaticInitialPositionY()
+{
+ loadTestData("transform_static.json");
+
+ QVERIFY(qFuzzyCompare(m_transform->position().y(), 50.0));
+}
+
+void tst_BMBasicTransform::testStaticInitialScaleX()
+{
+ loadTestData("transform_static.json");
+
+ QVERIFY(qFuzzyCompare(m_transform->scale().x(), 0.5));
+}
+
+void tst_BMBasicTransform::testStaticInitialScaleY()
+{
+ loadTestData("transform_static.json");
+
+ QVERIFY(qFuzzyCompare(m_transform->scale().y(), 0.5));
+}
+
+void tst_BMBasicTransform::testStaticInitialRotation()
+{
+ loadTestData("transform_static.json");
+
+ QVERIFY(qFuzzyCompare(m_transform->rotation(), 0.0));
+}
+
+void tst_BMBasicTransform::testStaticInitialOpacity()
+{
+ loadTestData("transform_static.json");
+
+ QVERIFY(qFuzzyCompare(m_transform->opacity(), 1.0));
+}
+
+void tst_BMBasicTransform::testStaticUpdatedAnchorX()
+{
+ loadTestData("transform_static.json");
+ updateProperty(180);
+
+ QVERIFY(qFuzzyCompare(m_transform->anchorPoint().x(), 50.0));
+}
+
+void tst_BMBasicTransform::testStaticUpdatedAnchorY()
+{
+ loadTestData("transform_static.json");
+ updateProperty(180);
+
+ QVERIFY(qFuzzyCompare(m_transform->anchorPoint().y(), 50.0));
+}
+
+void tst_BMBasicTransform::testStaticUpdatedPositionX()
+{
+ loadTestData("transform_static.json");
+ updateProperty(180);
+
+ QVERIFY(qFuzzyCompare(m_transform->position().x(), 50.0));
+}
+
+void tst_BMBasicTransform::testStaticUpdatedPositionY()
+{
+ loadTestData("transform_static.json");
+ updateProperty(180);
+
+ QVERIFY(qFuzzyCompare(m_transform->position().y(), 50.0));
+}
+
+void tst_BMBasicTransform::testStaticUpdatedScaleX()
+{
+ loadTestData("transform_static.json");
+ updateProperty(180);
+
+ QVERIFY(qFuzzyCompare(m_transform->scale().x(), 0.5));
+}
+
+void tst_BMBasicTransform::testStaticUpdatedScaleY()
+{
+ loadTestData("transform_static.json");
+ updateProperty(180);
+
+ QVERIFY(qFuzzyCompare(m_transform->scale().y(), 0.5));
+}
+
+void tst_BMBasicTransform::testStaticUpdatedRotation()
+{
+ loadTestData("transform_static.json");
+ updateProperty(180);
+
+ QVERIFY(qFuzzyCompare(m_transform->rotation(), 0.0));
+}
+
+void tst_BMBasicTransform::testStaticUpdatedOpacity()
+{
+ loadTestData("transform_static.json");
+ updateProperty(180);
+
+ QVERIFY(qFuzzyCompare(m_transform->opacity(), 1.0));
+}
+
+void tst_BMBasicTransform::testAnimatedInitialAnchorX()
+{
+ loadTestData("transform_animated.json");
+ updateProperty(0);
+
+ QVERIFY(qFuzzyCompare(m_transform->anchorPoint().x(), 50.0));
+}
+
+void tst_BMBasicTransform::testAnimatedInitialAnchorY()
+{
+ loadTestData("transform_animated.json");
+ updateProperty(0);
+
+ QVERIFY(qFuzzyCompare(m_transform->anchorPoint().y(), 50.0));
+}
+
+void tst_BMBasicTransform::testAnimatedInitialPositionX()
+{
+ loadTestData("transform_animated.json");
+ updateProperty(0);
+
+ QVERIFY(qFuzzyCompare(m_transform->position().x(), 50.0));
+}
+
+void tst_BMBasicTransform::testAnimatedInitialPositionY()
+{
+ loadTestData("transform_animated.json");
+ updateProperty(0);
+
+ QVERIFY(qFuzzyCompare(m_transform->position().y(), 50.0));
+}
+
+void tst_BMBasicTransform::testAnimatedInitialScaleX()
+{
+ loadTestData("transform_animated.json");
+ updateProperty(0);
+
+ QVERIFY(qFuzzyCompare(m_transform->scale().x(), 0.5));
+}
+
+void tst_BMBasicTransform::testAnimatedInitialScaleY()
+{
+ loadTestData("transform_animated.json");
+ updateProperty(0);
+
+ QVERIFY(qFuzzyCompare(m_transform->scale().y(), 0.5));
+}
+
+void tst_BMBasicTransform::testAnimatedInitialRotation()
+{
+ loadTestData("transform_animated.json");
+ updateProperty(0);
+
+ QVERIFY(qFuzzyCompare(m_transform->rotation(), 0.0));
+}
+
+void tst_BMBasicTransform::testAnimatedInitialOpacity()
+{
+ loadTestData("transform_animated.json");
+ updateProperty(0);
+
+ QVERIFY(qFuzzyCompare(m_transform->opacity(), 1.0));
+}
+
+void tst_BMBasicTransform::testAnimatedUpdatedAnchorX()
+{
+ loadTestData("transform_animated.json");
+ updateProperty(180);
+
+ QVERIFY(qFuzzyCompare(m_transform->anchorPoint().x(), 25.0));
+}
+
+void tst_BMBasicTransform::testAnimatedUpdatedAnchorY()
+{
+ loadTestData("transform_animated.json");
+ updateProperty(180);
+
+ QVERIFY(qFuzzyCompare(m_transform->anchorPoint().y(), 25.0));
+}
+
+void tst_BMBasicTransform::testAnimatedUpdatedPositionX()
+{
+ loadTestData("transform_animated.json");
+ updateProperty(180);
+
+ QVERIFY(qFuzzyCompare(m_transform->position().x(), 75.0));
+}
+
+void tst_BMBasicTransform::testAnimatedUpdatedPositionY()
+{
+ loadTestData("transform_animated.json");
+ updateProperty(180);
+
+ QVERIFY(qFuzzyCompare(m_transform->position().y(), 75.0));
+}
+
+void tst_BMBasicTransform::testAnimatedUpdatedScaleX()
+{
+ loadTestData("transform_animated.json");
+ updateProperty(180);
+
+ QVERIFY(qFuzzyCompare(m_transform->scale().x(), 1.0));
+}
+
+void tst_BMBasicTransform::testAnimatedUpdatedScaleY()
+{
+ loadTestData("transform_animated.json");
+ updateProperty(180);
+
+ QVERIFY(qFuzzyCompare(m_transform->scale().y(), 1.0));
+}
+
+void tst_BMBasicTransform::testAnimatedUpdatedRotation()
+{
+ loadTestData("transform_animated.json");
+ updateProperty(180);
+
+ QVERIFY(qFuzzyCompare(m_transform->rotation(), (3 * 360 + 30.0)));
+}
+
+void tst_BMBasicTransform::testAnimatedUpdatedOpacity()
+{
+ loadTestData("transform_animated.json");
+ updateProperty(180);
+
+ QVERIFY(qFuzzyCompare(m_transform->opacity(), 0.25));
+}
+
+void tst_BMBasicTransform::testActive()
+{
+ loadTestData("transform_static.json");
+ QVERIFY(m_transform->active(100) == true);
+}
+
+void tst_BMBasicTransform::loadTestData(const QByteArray &filename)
+{
+ if (m_transform) {
+ delete m_transform;
+ m_transform = nullptr;
+ }
+
+ QFile sourceFile(QFINDTESTDATA(filename.constData()));
+ if (!sourceFile.exists())
+ QFAIL("File does not exist");
+ if (!sourceFile.open(QIODevice::ReadOnly))
+ QFAIL("Cannot read test file");
+
+ QByteArray json = sourceFile.readAll();
+
+ sourceFile.close();
+
+ QJsonDocument doc = QJsonDocument::fromJson(json);
+ QJsonObject rootObj = doc.object();
+ if (rootObj.empty())
+ QFAIL("Cannot parse test file");
+
+ QJsonArray layers = rootObj.value(QLatin1String("layers")).toArray();
+ QJsonObject layerObj = layers[0].toObject();
+ int type = layerObj.value(QLatin1String("ty")).toInt();
+ if (type != 4)
+ QFAIL("It's not shape layer");
+
+ QJsonObject transformObj = layerObj.value(QLatin1String("ks")).toObject();
+ m_transform = new BMBasicTransform(transformObj);
+
+ QVERIFY(m_transform != nullptr);
+}
+
+void tst_BMBasicTransform::updateProperty(int frame)
+{
+ m_transform->updateProperties(frame);
+}
+
+QTEST_MAIN(tst_BMBasicTransform)
+#include "tst_bmbasictransform.moc"
diff --git a/tests/auto/bodymovin/shape/trimpath/trimpath.pro b/tests/auto/bodymovin/shape/trimpath/trimpath.pro
new file mode 100644
index 0000000..94ed1be
--- /dev/null
+++ b/tests/auto/bodymovin/shape/trimpath/trimpath.pro
@@ -0,0 +1,5 @@
+QT += testlib gui-private bodymovin-private
+CONFIG += c++11
+
+SOURCES = \
+ tst_bmtrimpath.cpp
diff --git a/tests/auto/bodymovin/shape/trimpath/trimpath_animated_2080_0_to_0060_3x30.json b/tests/auto/bodymovin/shape/trimpath/trimpath_animated_2080_0_to_0060_3x30.json
new file mode 100644
index 0000000..2532123
--- /dev/null
+++ b/tests/auto/bodymovin/shape/trimpath/trimpath_animated_2080_0_to_0060_3x30.json
@@ -0,0 +1 @@
+{"v":"5.3.4","fr":60,"ip":0,"op":180,"w":100,"h":100,"nm":"AnimatedTrimPathShapeLayer","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[50,50,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-29.992,10.008],[30.008,10]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[-12,25.75],[-14.75,-26.75]],"o":[[12,-25.75],[14.75,26.75]],"v":[[-31.25,-15.25],[28.5,-16]],"c":false},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":0,"s":[20],"e":[0]},{"t":179}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":0,"s":[80],"e":[60]},{"t":179}],"ix":2},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":0,"s":[0],"e":[1110]},{"t":179}],"ix":3},"m":2,"ix":3,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":-179,"op":184,"st":-179,"bm":0}],"markers":[]}
diff --git a/tests/auto/bodymovin/shape/trimpath/trimpath_hidden.json b/tests/auto/bodymovin/shape/trimpath/trimpath_hidden.json
new file mode 100644
index 0000000..7689dcc
--- /dev/null
+++ b/tests/auto/bodymovin/shape/trimpath/trimpath_hidden.json
@@ -0,0 +1 @@
+{"v":"5.3.4","fr":60,"ip":0,"op":180,"w":100,"h":100,"nm":"StaticTrimPathShapeLayer","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[50,50,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-29.992,10.008],[30.008,10]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tm","s":{"a":0,"k":20,"ix":1},"e":{"a":0,"k":80,"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":true}],"ip":-179,"op":1,"st":-179,"bm":0}],"markers":[]}
diff --git a/tests/auto/bodymovin/shape/trimpath/trimpath_static_20to80.json b/tests/auto/bodymovin/shape/trimpath/trimpath_static_20to80.json
new file mode 100644
index 0000000..bb7ac08
--- /dev/null
+++ b/tests/auto/bodymovin/shape/trimpath/trimpath_static_20to80.json
@@ -0,0 +1 @@
+{"v":"5.3.4","fr":60,"ip":0,"op":180,"w":100,"h":100,"nm":"StaticTrimPathShapeLayer","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[50,50,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-29.992,10.008],[30.008,10]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tm","s":{"a":0,"k":20,"ix":1},"e":{"a":0,"k":80,"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":-179,"op":1,"st":-179,"bm":0}],"markers":[]}
diff --git a/tests/auto/bodymovin/shape/trimpath/tst_bmtrimpath.cpp b/tests/auto/bodymovin/shape/trimpath/tst_bmtrimpath.cpp
new file mode 100644
index 0000000..6210c09
--- /dev/null
+++ b/tests/auto/bodymovin/shape/trimpath/tst_bmtrimpath.cpp
@@ -0,0 +1,311 @@
+/****************************************************************************
+**
+** Copyright (C) 2018 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the lottie-qt module of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:GPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 or (at your option) any later version
+** approved by the KDE Free Qt Foundation. The licenses are as published by
+** the Free Software Foundation and appearing in the file LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#include <QtTest/QtTest>
+
+#include <QFile>
+#include <QJsonDocument>
+#include <QJsonObject>
+#include <QJsonArray>
+
+#include "private/bmlayer_p.h"
+#include "private/bmtrimpath_p.h"
+
+class tst_BMTrimPath: public QObject
+{
+ Q_OBJECT
+
+public:
+ tst_BMTrimPath();
+ ~tst_BMTrimPath();
+
+private:
+
+ // void testParseStaticRect();
+
+private slots:
+ void initTestCase();
+ void cleanupTestCase();
+
+ void testStaticInitialStart();
+ void testStaticInitialEnd();
+ void testStaticInitialOffset();
+ void testStaticInitialSimultaneous();
+ void testStaticUpdatedStart();
+ void testStaticUpdatedEnd();
+ void testStaticUpdatedOffset();
+ void testStaticUpdatedSimultaneous();
+
+ void testAnimatedInitialStart();
+ void testAnimatedInitialEnd();
+ void testAnimatedInitialOffset();
+ void testAnimatedInitialSimultaneous();
+ void testAnimatedUpdatedStart();
+ void testAnimatedUpdatedEnd();
+ void testAnimatedUpdatedOffset();
+ void testAnimatedUpdatedSimultaneous();
+
+ void testName();
+ void testType();
+ void testHidden();
+ void testActive();
+
+private:
+ void loadTestData(const QByteArray &filename);
+ void updateProperty(int frame);
+
+ BMTrimPath *m_trimpath = nullptr;
+};
+
+tst_BMTrimPath::tst_BMTrimPath()
+{
+
+}
+
+tst_BMTrimPath::~tst_BMTrimPath()
+{
+
+}
+
+void tst_BMTrimPath::initTestCase()
+{
+}
+
+void tst_BMTrimPath::cleanupTestCase()
+{
+ if (m_trimpath)
+ delete m_trimpath;
+}
+
+void tst_BMTrimPath::testStaticInitialStart()
+{
+ loadTestData("trimpath_static_20to80.json");
+
+ QVERIFY(qFuzzyCompare(m_trimpath->start(), 20.0));
+}
+
+void tst_BMTrimPath::testStaticInitialEnd()
+{
+ loadTestData("trimpath_static_20to80.json");
+
+ QVERIFY(qFuzzyCompare(m_trimpath->end(), 80.0));
+}
+
+void tst_BMTrimPath::testStaticInitialOffset()
+{
+ loadTestData("trimpath_static_20to80.json");
+
+ QVERIFY(qFuzzyCompare(m_trimpath->offset(), 0.0));
+}
+
+void tst_BMTrimPath::testStaticInitialSimultaneous()
+{
+ loadTestData("trimpath_static_20to80.json");
+
+ QVERIFY(m_trimpath->simultaneous() == true);
+}
+
+void tst_BMTrimPath::testStaticUpdatedStart()
+{
+ loadTestData("trimpath_static_20to80.json");
+ updateProperty(180);
+
+ QVERIFY(qFuzzyCompare(m_trimpath->start(), 20.0));
+}
+
+void tst_BMTrimPath::testStaticUpdatedEnd()
+{
+ loadTestData("trimpath_static_20to80.json");
+ updateProperty(180);
+
+ QVERIFY(qFuzzyCompare(m_trimpath->end(), 80.0));
+}
+
+void tst_BMTrimPath::testStaticUpdatedOffset()
+{
+ loadTestData("trimpath_static_20to80.json");
+ updateProperty(180);
+
+ QVERIFY(qFuzzyCompare(m_trimpath->offset(), 0.0));
+}
+
+void tst_BMTrimPath::testStaticUpdatedSimultaneous()
+{
+ loadTestData("trimpath_static_20to80.json");
+ updateProperty(180);
+
+ QVERIFY(m_trimpath->simultaneous() == true);
+}
+
+void tst_BMTrimPath::testAnimatedInitialStart()
+{
+ loadTestData("trimpath_animated_2080_0_to_0060_3x30.json");
+ updateProperty(0);
+
+ QVERIFY(qFuzzyCompare(m_trimpath->start(), 20.0));
+}
+
+void tst_BMTrimPath::testAnimatedInitialEnd()
+{
+ loadTestData("trimpath_animated_2080_0_to_0060_3x30.json");
+ updateProperty(0);
+
+ QVERIFY(qFuzzyCompare(m_trimpath->end(), 80.0));
+}
+
+void tst_BMTrimPath::testAnimatedInitialOffset()
+{
+ loadTestData("trimpath_animated_2080_0_to_0060_3x30.json");
+ updateProperty(0);
+
+ QVERIFY(qFuzzyCompare(m_trimpath->offset(), 0.0));
+}
+
+void tst_BMTrimPath::testAnimatedInitialSimultaneous()
+{
+ loadTestData("trimpath_animated_2080_0_to_0060_3x30.json");
+ updateProperty(0);
+
+ QVERIFY(m_trimpath->simultaneous() == false);
+}
+
+void tst_BMTrimPath::testAnimatedUpdatedStart()
+{
+ loadTestData("trimpath_animated_2080_0_to_0060_3x30.json");
+ updateProperty(180);
+
+ QVERIFY(qFuzzyCompare(m_trimpath->start(), 0.0));
+}
+
+void tst_BMTrimPath::testAnimatedUpdatedEnd()
+{
+ loadTestData("trimpath_animated_2080_0_to_0060_3x30.json");
+ updateProperty(180);
+
+ QVERIFY(qFuzzyCompare(m_trimpath->end(), 60.0));
+}
+
+void tst_BMTrimPath::testAnimatedUpdatedOffset()
+{
+ loadTestData("trimpath_animated_2080_0_to_0060_3x30.json");
+ updateProperty(180);
+
+ QVERIFY(qFuzzyCompare(m_trimpath->offset(), (360 * 3 + 30.0)));
+}
+
+void tst_BMTrimPath::testAnimatedUpdatedSimultaneous()
+{
+ loadTestData("trimpath_animated_2080_0_to_0060_3x30.json");
+ updateProperty(180);
+
+ QVERIFY(m_trimpath->simultaneous() == false);
+}
+
+void tst_BMTrimPath::testName()
+{
+ loadTestData("trimpath_hidden.json");
+
+ QVERIFY(m_trimpath->name() == QString("Trim Paths 1"));
+}
+
+void tst_BMTrimPath::testType()
+{
+ loadTestData("trimpath_hidden.json");
+
+ QVERIFY(m_trimpath->type() == BM_SHAPE_TRIM_IX);
+}
+
+void tst_BMTrimPath::testHidden()
+{
+ loadTestData("trimpath_hidden.json");
+
+ QVERIFY(m_trimpath->hidden() == true);
+}
+
+void tst_BMTrimPath::testActive()
+{
+ loadTestData("trimpath_animated_2080_0_to_0060_3x30.json");
+ QVERIFY(m_trimpath->active(100) == true);
+
+ loadTestData("trimpath_hidden.json");
+ QVERIFY(m_trimpath->active(100) == false);
+}
+
+void tst_BMTrimPath::loadTestData(const QByteArray &filename)
+{
+ if (m_trimpath) {
+ delete m_trimpath;
+ m_trimpath = nullptr;
+ }
+
+ QFile sourceFile(QFINDTESTDATA(filename.constData()));
+ if (!sourceFile.exists())
+ QFAIL("File does not exist");
+ if (!sourceFile.open(QIODevice::ReadOnly))
+ QFAIL("Cannot read test file");
+
+ QByteArray json = sourceFile.readAll();
+
+ sourceFile.close();
+
+ QJsonDocument doc = QJsonDocument::fromJson(json);
+ QJsonObject rootObj = doc.object();
+ if (rootObj.empty())
+ QFAIL("Cannot parse test file");
+
+ QJsonArray layers = rootObj.value(QLatin1String("layers")).toArray();
+ QJsonObject layerObj = layers[0].toObject();
+ int type = layerObj.value(QLatin1String("ty")).toInt();
+ if (type != 4)
+ QFAIL("It's not shape layer");
+
+ QJsonArray shapes = layerObj.value(QLatin1String("shapes")).toArray();
+ QJsonArray::const_iterator shapesIt = shapes.constBegin();
+ BMShape* shape = nullptr;
+ while (shapesIt != shapes.end()) {
+ QJsonObject childObj = (*shapesIt).toObject();
+ shape = BMShape::construct(childObj);
+ QVERIFY(shape != nullptr);
+ if (shape->type() == BM_SHAPE_TRIM_IX)
+ break;
+ shapesIt++;
+ }
+
+ m_trimpath = static_cast<BMTrimPath*>(shape);
+
+ QVERIFY(m_trimpath != nullptr);
+}
+
+void tst_BMTrimPath::updateProperty(int frame)
+{
+ m_trimpath->updateProperties(frame);
+}
+
+QTEST_MAIN(tst_BMTrimPath)
+#include "tst_bmtrimpath.moc"
diff --git a/tests/auto/imports/imports.pro b/tests/auto/imports/imports.pro
new file mode 100644
index 0000000..6b78b4c
--- /dev/null
+++ b/tests/auto/imports/imports.pro
@@ -0,0 +1,30 @@
+QT += quick
+CONFIG += c++11 qmltestcase
+
+# The following define makes your compiler emit warnings if you use
+# any feature of Qt which as been marked deprecated (the exact warnings
+# depend on your compiler). Please consult the documentation of the
+# deprecated API in order to know how to port your code away from it.
+DEFINES += QT_DEPRECATED_WARNINGS
+
+# You can also make your code fail to compile if you use deprecated APIs.
+# In order to do so, uncomment the following line.
+# You can also select to disable deprecated APIs only up to a certain version of Qt.
+#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0
+
+SOURCES += \
+ main.cpp
+
+RESOURCES += qml.qrc
+
+# Additional import path used to resolve QML modules in Qt Creator's code model
+QML_IMPORT_PATH =
+
+# Additional import path used to resolve QML modules just for Qt Quick Designer
+QML_DESIGNER_IMPORT_PATH =
+
+# Default rules for deployment.
+qnx: target.path = /tmp/$${TARGET}/bin
+else: unix:!android: target.path = /opt/$${TARGET}/bin
+!isEmpty(target.path): INSTALLS += target
+
diff --git a/tests/auto/imports/main.cpp b/tests/auto/imports/main.cpp
new file mode 100644
index 0000000..a7e37a8
--- /dev/null
+++ b/tests/auto/imports/main.cpp
@@ -0,0 +1,3 @@
+#include <QtQuickTest/quicktest.h>
+
+QUICK_TEST_MAIN("tst_main.qml");
diff --git a/tests/auto/imports/qml.qrc b/tests/auto/imports/qml.qrc
new file mode 100644
index 0000000..c821f84
--- /dev/null
+++ b/tests/auto/imports/qml.qrc
@@ -0,0 +1,6 @@
+<RCC>
+ <qresource prefix="/">
+ <file>rec_pos_col_opa.json</file>
+ <file>tst_main.qml</file>
+ </qresource>
+</RCC>
diff --git a/tests/auto/imports/rec_pos_col_opa.json b/tests/auto/imports/rec_pos_col_opa.json
new file mode 100644
index 0000000..35245f4
--- /dev/null
+++ b/tests/auto/imports/rec_pos_col_opa.json
@@ -0,0 +1 @@
+{"v":"5.3.4","fr":60,"ip":60,"op":180,"w":100,"h":100,"nm":"Test","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[50,50,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[40,40],"ix":2},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":60,"s":[0,0],"e":[25,-25],"to":[4.16666650772095,-4.16666650772095],"ti":[-4.16666650772095,4.16666650772095]},{"t":119}],"ix":3},"r":{"a":0,"k":0,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"st","c":{"a":0,"k":[1,0,0,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":0,"ix":5},"lc":1,"lj":1,"ml":4,"ml2":{"a":0,"k":4,"ix":8},"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":60,"s":[1,0,0,1],"e":[0,0,1,1]},{"t":119}],"ix":4},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":60,"s":[100],"e":[25]},{"t":119}],"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":60,"op":120,"st":0,"bm":0}],"markers":[]}
diff --git a/tests/auto/imports/tst_main.qml b/tests/auto/imports/tst_main.qml
new file mode 100644
index 0000000..788d385
--- /dev/null
+++ b/tests/auto/imports/tst_main.qml
@@ -0,0 +1,88 @@
+/****************************************************************************
+**
+** Copyright (C) 2018 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the lottie-qt module of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:GPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 or (at your option) any later version
+** approved by the KDE Free Qt Foundation. The licenses are as published by
+** the Free Software Foundation and appearing in the file LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+import QtQuick 2.9
+import QtQuick.Window 2.2
+import QtTest 1.0
+import Qt.labs.lottieqt 1.0
+
+Item {
+ id: animContainer
+ width: childrenRect.width
+ height: childrenRect.height
+
+ LottieAnimation {
+ id: bmAnim
+
+ x: 0
+ y: 0
+ quality: LottieAnimation.HighQuality
+ source: ":/rec_pos_col_opa.json"
+
+ onFinished: {
+ bmAnim.start();
+ }
+
+
+ }
+
+ TestCase{
+ id: testID
+ name: "testAnimation"
+
+ function test_1_loadingStatus(){
+ compare(bmAnim.status, 2); /* status: Null(0), Loading(1), Ready(2), Error(3)
+ if the source has been loaded successfully, the status
+ must be "Ready"
+ */
+ }
+
+ function test_2_initialFrameRate(){
+ compare(bmAnim.frameRate, 60); /* frame rate of the source used in this test is 60 */
+ }
+
+ function test_3_changeFrameRate(){
+ bmAnim.frameRate = 30;
+ compare(bmAnim.frameRate, 30);
+ }
+
+ function test_4_initialQuality(){
+ compare(bmAnim.quality, 2); /* quality: LowQuality(0), MediumQuality(1), HighQuality(2)
+ lottieanimation's initial quality is set to HighQuality
+ */
+ }
+
+ function test_5_changeQuality(){
+ bmAnim.quality = 1; /* change quality to MediumQuality */
+ compare(bmAnim.quality, 1);
+ }
+
+
+ }
+}
diff --git a/tests/manual/featcheck/.gitignore b/tests/manual/featcheck/.gitignore
new file mode 100644
index 0000000..fab7372
--- /dev/null
+++ b/tests/manual/featcheck/.gitignore
@@ -0,0 +1,73 @@
+# This file is used to ignore files which are generated
+# ----------------------------------------------------------------------------
+
+*~
+*.autosave
+*.a
+*.core
+*.moc
+*.o
+*.obj
+*.orig
+*.rej
+*.so
+*.so.*
+*_pch.h.cpp
+*_resource.rc
+*.qm
+.#*
+*.*#
+core
+!core/
+tags
+.DS_Store
+.directory
+*.debug
+Makefile*
+*.prl
+*.app
+moc_*.cpp
+ui_*.h
+qrc_*.cpp
+Thumbs.db
+*.res
+*.rc
+/.qmake.cache
+/.qmake.stash
+
+# qtcreator generated files
+*.pro.user*
+
+# xemacs temporary files
+*.flc
+
+# Vim temporary files
+.*.swp
+
+# Visual Studio generated files
+*.ib_pdb_index
+*.idb
+*.ilk
+*.pdb
+*.sln
+*.suo
+*.vcproj
+*vcproj.*.*.user
+*.ncb
+*.sdf
+*.opensdf
+*.vcxproj
+*vcxproj.*
+
+# MinGW generated files
+*.Debug
+*.Release
+
+# Python byte code
+*.pyc
+
+# Binaries
+# --------
+*.dll
+*.exe
+
diff --git a/tests/manual/featcheck/featcheck.pro b/tests/manual/featcheck/featcheck.pro
new file mode 100644
index 0000000..2a35170
--- /dev/null
+++ b/tests/manual/featcheck/featcheck.pro
@@ -0,0 +1,22 @@
+QT += bodymovin-private
+CONFIG += c++11 console
+CONFIG -= app_bundle
+
+# The following define makes your compiler emit warnings if you use
+# any feature of Qt which as been marked deprecated (the exact warnings
+# depend on your compiler). Please consult the documentation of the
+# deprecated API in order to know how to port your code away from it.
+DEFINES += QT_DEPRECATED_WARNINGS
+
+# You can also make your code fail to compile if you use deprecated APIs.
+# In order to do so, uncomment the following line.
+# You can also select to disable deprecated APIs only up to a certain version of Qt.
+#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0
+
+SOURCES += \
+ main.cpp
+
+# Default rules for deployment.
+qnx: target.path = /tmp/$${TARGET}/bin
+else: unix:!android: target.path = /opt/$${TARGET}/bin
+!isEmpty(target.path): INSTALLS += target
diff --git a/tests/manual/featcheck/howtofind.txt b/tests/manual/featcheck/howtofind.txt
new file mode 100644
index 0000000..6879b85
--- /dev/null
+++ b/tests/manual/featcheck/howtofind.txt
@@ -0,0 +1,7 @@
+
+1. Build featcheck
+2. Add build directory to path
+3. Switch to the directory containing json files (in subdirectories)
+4. Enter command
+ find * -name *.json -printf '\n\n%p:\n\n' -exec featcheck {} \; &>log
+5. The results are now found in the log file
diff --git a/tests/manual/featcheck/main.cpp b/tests/manual/featcheck/main.cpp
new file mode 100644
index 0000000..cdd7f3a
--- /dev/null
+++ b/tests/manual/featcheck/main.cpp
@@ -0,0 +1,82 @@
+/****************************************************************************
+**
+** Copyright (C) 2018 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the lottie-qt module of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:GPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 or (at your option) any later version
+** approved by the KDE Free Qt Foundation. The licenses are as published by
+** the Free Software Foundation and appearing in the file LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#include <QCoreApplication>
+#include <QJsonDocument>
+#include <QJsonObject>
+#include <QJsonArray>
+
+#include "private/bmbase_p.h"
+#include "private/bmlayer_p.h"
+
+int main(int argc, char *argv[])
+{
+ QCoreApplication a(argc, argv);
+
+ if (argc < 2) {
+ printf("Filename missing\n");
+ return -1;
+ }
+
+ QFile sourceFile(argv[1]);
+
+ if (!sourceFile.open(QIODevice::ReadOnly)) {
+ fprintf(stderr, "Cannot open file\n");
+ return -1;
+ }
+
+ QByteArray json = sourceFile.readAll();
+
+ QJsonDocument doc = QJsonDocument::fromJson(json);
+ QJsonObject rootObj = doc.object();
+
+ if (rootObj.empty()) {
+ fprintf(stderr, "Json document not found\n");
+ return -1;
+ }
+
+ if (rootObj.value(QLatin1String("assets")).toArray().count())
+ qWarning() << "assets not supported";
+
+ if (rootObj.value(QLatin1String("chars")).toArray().count())
+ qWarning() << "chars not supported";
+
+ if (rootObj.value(QLatin1String("markers")).toArray().count())
+ qWarning() << "markers not supported";
+
+ QJsonArray jsonLayers = rootObj.value(QLatin1String("layers")).toArray();
+ QJsonArray::const_iterator jsonLayerIt = jsonLayers.constBegin();
+ while (jsonLayerIt != jsonLayers.constEnd()) {
+ QJsonObject jsonLayer = (*jsonLayerIt).toObject();
+ BMLayer *layer = BMLayer::construct(jsonLayer);
+ if (layer)
+ delete layer;
+ jsonLayerIt++;
+ }
+}
diff --git a/tests/manual/html/content.json b/tests/manual/html/content.json
new file mode 100644
index 0000000..0a49e5e
--- /dev/null
+++ b/tests/manual/html/content.json
@@ -0,0 +1 @@
+{"v":"5.3.1","fr":29.9700012207031,"ip":0,"op":114.000004643315,"w":356,"h":200,"nm":"Comp 1","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":0,"s":[0],"e":[63]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":45,"s":[63],"e":[0]},{"t":90.0000036657751}],"ix":10},"p":{"a":0,"k":[178,100,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[54.5,66.5],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":0,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":1,"lj":1,"ml":4,"ml2":{"a":0,"k":4,"ix":8},"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":114.000004643315,"st":0,"bm":0}],"markers":[]}
diff --git a/tests/manual/html/index.html b/tests/manual/html/index.html
new file mode 100644
index 0000000..b0f6ee6
--- /dev/null
+++ b/tests/manual/html/index.html
@@ -0,0 +1,47 @@
+<html xmlns="http://www.w3.org/1999/xhtml" />
+<meta charset="UTF-8">
+
+<head>
+ <style>
+ body,
+ html {
+ background-color: #ffffff;
+ color: #000000;
+ margin: 0px;
+ height: 100%;
+ width: 100%;
+ overflow-x: hidden;
+ overflow-y: hidden;
+ font-family: Arial;
+ font-weight: bold;
+ }
+ </style>
+ <script src=https://cdnjs.cloudflare.com/ajax/libs/bodymovin/5.4.1/lottie.js type="text/javascript"></script>
+</head>
+
+<body>
+
+<div class="position">
+ <div class="title">CONTENT</div>
+ <div class="bodymovin" id="content"></div>
+</div>
+
+ <script>
+ var content = {
+ container: document.getElementById('content'),
+ renderer: 'svg',
+ loop: true,
+ autoplay: true,
+ rendererSettings: {
+ progressiveLoad: false
+ },
+ path: 'content.json'
+ };
+
+ var anim = bodymovin.loadAnimation(content);
+ </script>
+
+
+</body>
+
+</html>
diff --git a/tests/manual/manual.pro b/tests/manual/manual.pro
new file mode 100644
index 0000000..b2c0eb1
--- /dev/null
+++ b/tests/manual/manual.pro
@@ -0,0 +1,2 @@
+TEMPLATE = subdirs
+SUBDIRS += testApp
diff --git a/tests/manual/testApp/.gitignore b/tests/manual/testApp/.gitignore
new file mode 100644
index 0000000..fab7372
--- /dev/null
+++ b/tests/manual/testApp/.gitignore
@@ -0,0 +1,73 @@
+# This file is used to ignore files which are generated
+# ----------------------------------------------------------------------------
+
+*~
+*.autosave
+*.a
+*.core
+*.moc
+*.o
+*.obj
+*.orig
+*.rej
+*.so
+*.so.*
+*_pch.h.cpp
+*_resource.rc
+*.qm
+.#*
+*.*#
+core
+!core/
+tags
+.DS_Store
+.directory
+*.debug
+Makefile*
+*.prl
+*.app
+moc_*.cpp
+ui_*.h
+qrc_*.cpp
+Thumbs.db
+*.res
+*.rc
+/.qmake.cache
+/.qmake.stash
+
+# qtcreator generated files
+*.pro.user*
+
+# xemacs temporary files
+*.flc
+
+# Vim temporary files
+.*.swp
+
+# Visual Studio generated files
+*.ib_pdb_index
+*.idb
+*.ilk
+*.pdb
+*.sln
+*.suo
+*.vcproj
+*vcproj.*.*.user
+*.ncb
+*.sdf
+*.opensdf
+*.vcxproj
+*vcxproj.*
+
+# MinGW generated files
+*.Debug
+*.Release
+
+# Python byte code
+*.pyc
+
+# Binaries
+# --------
+*.dll
+*.exe
+
diff --git a/tests/manual/testApp/easing_rect_translate.json b/tests/manual/testApp/easing_rect_translate.json
new file mode 100644
index 0000000..2cede5b
--- /dev/null
+++ b/tests/manual/testApp/easing_rect_translate.json
@@ -0,0 +1 @@
+{"v":"5.3.1","fr":29.9700012207031,"ip":0,"op":175.000007127896,"w":400,"h":224,"nm":"rect translate","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[200,112,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[50,-31.75],[-50,-31.75],[-50,31.75],[50,31.75]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":1,"ix":5},"lc":1,"lj":1,"ml":4,"ml2":{"a":0,"k":4,"ix":8},"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.23529399797,1,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":1,"k":[{"i":{"x":0.939,"y":0},"o":{"x":0.958,"y":0.066},"n":"0p939_0_0p958_0p066","t":0,"s":[-37.5,-1.25],"e":[137.5,-1.25],"to":[29.1666660308838,0],"ti":[-29.1666660308838,0]},{"t":168.00000684278}],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":175.000007127896,"st":0,"bm":0}],"markers":[]}
diff --git a/tests/manual/testApp/gradient_fill.json b/tests/manual/testApp/gradient_fill.json
new file mode 100644
index 0000000..8351a5b
--- /dev/null
+++ b/tests/manual/testApp/gradient_fill.json
@@ -0,0 +1 @@
+{"v":"5.3.1","fr":29.9700012207031,"ip":0,"op":241.000009816131,"w":356,"h":200,"nm":"Comp 1","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[106.5,71.5,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[103.689,91.954,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[43.5,43.5],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":0,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":1,"lj":1,"ml":4,"ml2":{"a":0,"k":4,"ix":8},"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":41,"ix":4},"sa":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":0,"s":[0],"e":[191]},{"t":139.000005661586}],"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":2,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gf","o":{"a":0,"k":100,"ix":10},"r":1,"g":{"p":3,"k":{"a":0,"k":[0,1,0,0,0.5,0.5,0,0.5,1,0,0,1],"ix":9}},"s":{"a":0,"k":[2,0],"ix":5},"e":{"a":0,"k":[15,6],"ix":6},"t":2,"h":{"a":0,"k":84,"ix":7},"a":{"a":0,"k":-7,"ix":8},"nm":"Gradient Fill 1","mn":"ADBE Vector Graphic - G-Fill","hd":false}],"ip":0,"op":175.000007127896,"st":0,"bm":0}],"markers":[]}
diff --git a/tests/manual/testApp/layer_effect_fill.json b/tests/manual/testApp/layer_effect_fill.json
new file mode 100644
index 0000000..36e4821
--- /dev/null
+++ b/tests/manual/testApp/layer_effect_fill.json
@@ -0,0 +1 @@
+{"v":"5.3.1","fr":29.9700012207031,"ip":0,"op":175.000007127896,"w":400,"h":224,"nm":"Comp 1","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[200,112,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"ef":[{"ty":21,"nm":"Fill","np":9,"mn":"ADBE Fill","ix":1,"en":1,"ef":[{"ty":10,"nm":"Fill Mask","mn":"ADBE Fill-0001","ix":1,"v":{"a":0,"k":0,"ix":1}},{"ty":7,"nm":"All Masks","mn":"ADBE Fill-0007","ix":2,"v":{"a":0,"k":0,"ix":2}},{"ty":2,"nm":"Color","mn":"ADBE Fill-0002","ix":3,"v":{"a":0,"k":[1,0.223529413342,0.168627455831,1],"ix":3}},{"ty":7,"nm":"Invert","mn":"ADBE Fill-0006","ix":4,"v":{"a":0,"k":0,"ix":4}},{"ty":0,"nm":"Horizontal Feather","mn":"ADBE Fill-0003","ix":5,"v":{"a":0,"k":0,"ix":5}},{"ty":0,"nm":"Vertical Feather","mn":"ADBE Fill-0004","ix":6,"v":{"a":0,"k":0,"ix":6}},{"ty":0,"nm":"Opacity","mn":"ADBE Fill-0005","ix":7,"v":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":0,"s":[1],"e":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":60,"s":[0],"e":[1]},{"t":110.000004480392}],"ix":7}}]},{"ty":5,"nm":"Checkbox Control","np":3,"mn":"ADBE Checkbox Control","ix":2,"en":1,"ef":[{"ty":7,"nm":"Checkbox","mn":"ADBE Checkbox Control-0001","ix":1,"v":{"a":0,"k":1,"ix":1}}]},{"ty":5,"nm":"Slider Control","np":3,"mn":"ADBE Slider Control","ix":3,"en":1,"ef":[{"ty":0,"nm":"Slider","mn":"ADBE Slider Control-0001","ix":1,"v":{"a":0,"k":0,"ix":1}}]}],"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[43.5,-53.5],[-43.5,-53.5],[-43.5,53.5],[43.5,53.5]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4,"ix":5},"lc":1,"lj":1,"ml":4,"ml2":{"a":0,"k":4,"ix":8},"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.235294117647,1,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[116.5,2.5],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 2","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[57,-44],[-57,-44],[-57,44],[57,44]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4,"ix":5},"lc":1,"lj":1,"ml":4,"ml2":{"a":0,"k":4,"ix":8},"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.235294117647,1,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-52,-15],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":3,"cix":2,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":175.000007127896,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Shape Layer 2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[200,112,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"ef":[{"ty":21,"nm":"Fill","np":9,"mn":"ADBE Fill","ix":1,"en":1,"ef":[{"ty":10,"nm":"Fill Mask","mn":"ADBE Fill-0001","ix":1,"v":{"a":0,"k":0,"ix":1}},{"ty":7,"nm":"All Masks","mn":"ADBE Fill-0007","ix":2,"v":{"a":0,"k":0,"ix":2}},{"ty":2,"nm":"Color","mn":"ADBE Fill-0002","ix":3,"v":{"a":0,"k":[1,0.823529422283,0.121568627656,1],"ix":3}},{"ty":7,"nm":"Invert","mn":"ADBE Fill-0006","ix":4,"v":{"a":0,"k":0,"ix":4}},{"ty":0,"nm":"Horizontal Feather","mn":"ADBE Fill-0003","ix":5,"v":{"a":0,"k":0,"ix":5}},{"ty":0,"nm":"Vertical Feather","mn":"ADBE Fill-0004","ix":6,"v":{"a":0,"k":0,"ix":6}},{"ty":0,"nm":"Opacity","mn":"ADBE Fill-0005","ix":7,"v":{"a":0,"k":1,"ix":7}}]}],"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[129,-35.5],[-129,-35.5],[-129,35.5],[129,35.5]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0,0.027450980619,0.854901969433,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":15,"ix":5},"lc":1,"lj":1,"ml":4,"ml2":{"a":0,"k":4,"ix":8},"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.23529399797,1,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-25,-52.5],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":175.000007127896,"st":0,"bm":0}],"markers":[]}
diff --git a/tests/manual/testApp/layer_effect_slider.json b/tests/manual/testApp/layer_effect_slider.json
new file mode 100644
index 0000000..14b7da7
--- /dev/null
+++ b/tests/manual/testApp/layer_effect_slider.json
@@ -0,0 +1 @@
+{"v":"5.3.1","fr":29.9700012207031,"ip":0,"op":175.000007127896,"w":400,"h":224,"nm":"Comp 1","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[200,112,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"ef":[{"ty":5,"nm":"Slider Control","np":3,"mn":"ADBE Slider Control","ix":1,"en":1,"ef":[{"ty":0,"nm":"Slider","mn":"ADBE Slider Control-0001","ix":1,"v":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":0,"s":[100],"e":[46.696]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":42,"s":[46.696],"e":[100]},{"t":83.0000033806593}],"ix":1}}]},{"ty":5,"nm":"Slider Control 2","np":3,"mn":"ADBE Slider Control","ix":2,"en":1,"ef":[{"ty":0,"nm":"Slider","mn":"ADBE Slider Control-0001","ix":1,"v":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":0,"s":[100],"e":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":21,"s":[0],"e":[96.916]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":60,"s":[96.916],"e":[0]},{"t":83.0000033806593}],"ix":1}}]}],"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[100,100],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":0,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[95,-10],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3,"x":"var $bm_rt;\n$bm_rt = content('Group 1').transform.scale;"},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7,"x":"var $bm_rt;\n$bm_rt = effect('Slider Control')('Slider');"},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[63.5,-33.5],[-63.5,-33.5],[-63.5,33.5],[63.5,33.5]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4,"ix":5},"lc":1,"lj":1,"ml":4,"ml2":{"a":0,"k":4,"ix":8},"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.23529399797,1,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-114.5,-0.5],"ix":2,"x":"var $bm_rt;\nvar temp;\ntemp = effect('Slider Control 2')('Slider');\n$bm_rt = [\n temp,\n temp\n];"},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7,"x":"var $bm_rt;\n$bm_rt = effect('Slider Control')('Slider');"},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":3,"cix":2,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":175.000007127896,"st":0,"bm":0}],"markers":[]}
diff --git a/tests/manual/testApp/linked_layers.json b/tests/manual/testApp/linked_layers.json
new file mode 100644
index 0000000..44886c9
--- /dev/null
+++ b/tests/manual/testApp/linked_layers.json
@@ -0,0 +1 @@
+{"v":"5.3.1","fr":29.9700012207031,"ip":10.0000004073083,"op":175.000007127896,"w":400,"h":224,"nm":"Comp 1","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 2","parent":2,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":20,"s":[150,0,0],"e":[0,0,0],"to":[-37.2210121154785,-3.5262824391979e-15,0],"ti":[21.4031734466553,2.0277157723221e-15,0]},{"t":110.000004480392}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"rc","d":1,"s":{"a":0,"k":[100,100],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":0,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.874509811401,0.082352943718,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false}],"ip":-15.0000006109625,"op":175.000007127896,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":21,"s":[50,112,0],"e":[200,112,0],"to":[37.6828193664551,0,0],"ti":[-21.1501274108887,0,0]},{"t":110.000004480392}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"rc","d":1,"s":{"a":0,"k":[100,100],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":0,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false}],"ip":-17.0000006924242,"op":175.000007127896,"st":0,"bm":0}],"markers":[]}
diff --git a/tests/manual/testApp/main.cpp b/tests/manual/testApp/main.cpp
new file mode 100644
index 0000000..c07f125
--- /dev/null
+++ b/tests/manual/testApp/main.cpp
@@ -0,0 +1,50 @@
+/****************************************************************************
+**
+** Copyright (C) 2018 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the lottie-qt module of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:GPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 or (at your option) any later version
+** approved by the KDE Free Qt Foundation. The licenses are as published by
+** the Free Software Foundation and appearing in the file LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#include <QGuiApplication>
+#include <QQmlApplicationEngine>
+
+int main(int argc, char *argv[])
+{
+ if (qEnvironmentVariableIsEmpty("QTGLESSTREAM_DISPLAY")) {
+ qputenv("QT_QPA_EGLFS_PHYSICAL_WIDTH", QByteArray("213"));
+ qputenv("QT_QPA_EGLFS_PHYSICAL_HEIGHT", QByteArray("120"));
+
+ QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
+ }
+
+ QGuiApplication app(argc, argv);
+
+ QQmlApplicationEngine engine;
+ engine.load(QUrl(QStringLiteral("qrc:/main.qml")));
+ if (engine.rootObjects().isEmpty())
+ return -1;
+
+ return app.exec();
+}
diff --git a/tests/manual/testApp/main.qml b/tests/manual/testApp/main.qml
new file mode 100644
index 0000000..fdc1878
--- /dev/null
+++ b/tests/manual/testApp/main.qml
@@ -0,0 +1,132 @@
+/****************************************************************************
+**
+** Copyright (C) 2018 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the lottie-qt module of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:GPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 or (at your option) any later version
+** approved by the KDE Free Qt Foundation. The licenses are as published by
+** the Free Software Foundation and appearing in the file LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+import QtQuick 2.9
+import QtQuick.Window 2.2
+import Qt.labs.lottieqt 1.0
+
+Window {
+ visible: true
+ width: animContainer.width
+ height: animContainer.height
+ title: qsTr("Animation test")
+
+ property bool showFps: false
+
+ color: "black"
+
+ Item {
+ id: animContainer
+ width: childrenRect.width
+ height: childrenRect.height
+
+ Repeater {
+ model: 1
+
+ LottieAnimation {
+ id: bmAnim
+
+ x: 10 * index
+ y: 10 * index
+ loops: LottieAnimation.Infinite
+ quality: LottieAnimation.MediumQuality
+ source: ":/rect_rotate.json"
+ }
+ }
+ }
+
+ Text {
+ id: text
+
+ property real t
+ property int frame: 0
+
+ anchors.right: parent.right
+ color: "red"
+ visible: showFps
+ text: "FPS: " + fpsTimer.fps
+
+ Timer {
+ id: fpsTimer
+ property real fps: 0
+ repeat: true
+ interval: 1000
+ running: showFps
+ onTriggered: {
+ parent.text = "FPS: " + fpsTimer.fps
+ fps = text.frame
+ text.frame = 0
+ }
+ }
+
+ NumberAnimation on t {
+ id: tAnim
+ from: 0
+ to: 1000
+ running: showFps
+ loops: Animation.Infinite
+ }
+
+ onTChanged: {
+ update() // force continuous animation
+ text.frame++;
+ }
+ }
+
+ Rectangle {
+ id: rot
+
+ width: 10
+ height: 100
+ anchors.centerIn: parent
+ color: "blue"
+ enabled: showFps
+ visible: enabled
+
+ PropertyAnimation {
+ target: rot
+ property: "rotation"
+ from: 0
+ to: 360
+ duration: 500
+ running: true
+ loops: Animation.Infinite
+ }
+ }
+
+ PropertyAnimation {
+ target: rot
+ property: "rotation"
+ from: 0
+ to: 360
+ duration: 500
+ running: true
+ loops: Animation.Infinite
+ }
+}
diff --git a/tests/manual/testApp/path_and_ellipse.json b/tests/manual/testApp/path_and_ellipse.json
new file mode 100644
index 0000000..7332d97
--- /dev/null
+++ b/tests/manual/testApp/path_and_ellipse.json
@@ -0,0 +1 @@
+{"v":"5.3.1","fr":29.9700012207031,"ip":0,"op":175.000007127896,"w":550,"h":309,"nm":"Comp 1","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[460,150.5,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"d":1,"ty":"el","s":{"a":0,"k":[100,100],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0.027450980619,0.996078431606,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 2","mn":"ADBE Vector Graphic - Fill","hd":false},{"ind":2,"ty":"sh","ix":3,"ks":{"a":0,"k":{"i":[],"o":[],"v":[],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":3,"ty":"sh","ix":4,"ks":{"a":0,"k":{"i":[[96,1],[0,0],[17.091,27.482],[0,0]],"o":[[-96,-1],[0,0],[-17.091,-27.482],[0,0]],"v":[[-311,-13.5],[-258,54.5],[-245.909,9.982],[-236,-26.5]],"c":false},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false}],"ip":0,"op":175.000007127896,"st":0,"bm":0}],"markers":[]}
diff --git a/tests/manual/testApp/qml.qrc b/tests/manual/testApp/qml.qrc
new file mode 100644
index 0000000..b678a61
--- /dev/null
+++ b/tests/manual/testApp/qml.qrc
@@ -0,0 +1,45 @@
+<RCC>
+ <qresource prefix="/">
+ <file>main.qml</file>
+ <file>rect_anim_easing.json</file>
+ <file>rect_move_bezier.json</file>
+ <file>rect_anchor_moved_static2.json</file>
+ <file>rect_anchor_std_anim.json</file>
+ <file>shape_static.json</file>
+ <file>shape_circle.json</file>
+ <file>rect_rotate.json</file>
+ <file>shape_complex.json</file>
+ <file>shape_complex2.json</file>
+ <file>rect_skew_axis.json</file>
+ <file>rect_skew.json</file>
+ <file>rect_gfill_radial.json</file>
+ <file>rect_gfill_linear.json</file>
+ <file>trim_path1.json</file>
+ <file>trim_path2.json</file>
+ <file>trim_path2_individual.json</file>
+ <file>path_and_ellipse.json</file>
+ <file>trim_path2_offset.json</file>
+ <file>trim_path_multiple1.json</file>
+ <file>trim_path_multiple2.json</file>
+ <file>trim_path_multiple3.json</file>
+ <file>trim_path_multiple4.json</file>
+ <file>rect_negative_scale_anchors.json</file>
+ <file>rect_negative_scale_pos.json</file>
+ <file>rect_negative_scale_pos_anchors_rot.json</file>
+ <file>rect_rotate_shape.json</file>
+ <file>rect_rotate_layer.json</file>
+ <file>rect_rotate_layer2.json</file>
+ <file>trim_path_simultaneous2.json</file>
+ <file>shape_bezier_simple.json</file>
+ <file>layer_effect_fill.json</file>
+ <file>layer_effect_slider.json</file>
+ <file>easing_rect_translate.json</file>
+ <file>repeater1.json</file>
+ <file>repeater_group1.json</file>
+ <file>repeater_group2.json</file>
+ <file>rect_scale.json</file>
+ <file>rect_scale_anchors_on_layer.json</file>
+ <file>rect_scale_anchors_on_rect.json</file>
+ <file>linked_layers.json</file>
+ </qresource>
+</RCC>
diff --git a/tests/manual/testApp/rect_anchor_moved_static2.json b/tests/manual/testApp/rect_anchor_moved_static2.json
new file mode 100644
index 0000000..f451d5f
--- /dev/null
+++ b/tests/manual/testApp/rect_anchor_moved_static2.json
@@ -0,0 +1 @@
+{"v":"5.3.1","fr":29.9700012207031,"ip":0,"op":241.000009816131,"w":356,"h":200,"nm":"Comp 1","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,0,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[43.5,43.5],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":0,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":1,"lj":1,"ml":4,"ml2":{"a":0,"k":4,"ix":8},"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[-43.5,-43.5],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":175.000007127896,"st":0,"bm":0}],"markers":[]}
diff --git a/tests/manual/testApp/rect_anchor_std_anim.json b/tests/manual/testApp/rect_anchor_std_anim.json
new file mode 100644
index 0000000..8429b25
--- /dev/null
+++ b/tests/manual/testApp/rect_anchor_std_anim.json
@@ -0,0 +1 @@
+{"v":"5.3.1","fr":29.9700012207031,"ip":0,"op":175.000007127896,"w":356,"h":200,"nm":"Comp 1","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,0,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[43.5,43.5],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":0,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":1,"lj":1,"ml":4,"ml2":{"a":0,"k":4,"ix":8},"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":0,"s":[0,0],"e":[-43.5,-43.5],"to":[-7.25,-7.25],"ti":[0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":70,"s":[-43.5,-43.5],"e":[0,0],"to":[0,0],"ti":[-7.25,-7.25]},{"t":119.000004846969}],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":147.000005987433,"st":0,"bm":0}],"markers":[]}
diff --git a/tests/manual/testApp/rect_anim_easing.json b/tests/manual/testApp/rect_anim_easing.json
new file mode 100644
index 0000000..55e6560
--- /dev/null
+++ b/tests/manual/testApp/rect_anim_easing.json
@@ -0,0 +1,2 @@
+{"v":"5.3.1","fr":29.9700012207031,"ip":22.0000008960784,"op":517.000021057842,"w":1920,"h":1080,"nm":"Comp 1","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"n":"0p667_1_0p333_0","t":22.355,"s":[652.714,831.376,0],"e":[761.705,991.696,0],"to":[-35.3866271972656,-331.209503173828,0],"ti":[421.808654785156,-54.9365158081055,0]},{"i":{"x":0.703,"y":1},"o":{"x":0.333,"y":0},"n":"0p703_1_0p333_0","t":76,"s":[761.705,991.696,0],"e":[1066.115,493.603,0],"to":[-50.1014442443848,6.525230884552,0],"ti":[-401.992126464844,-38.2022514343262,0]},{"t":109.741254469853}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[273.185,168.44],"ix":2,"x":"var $bm_rt;\n$bm_rt = content('Rectangle 1').content('Rectangle Path 1').size;"},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":0,"s":[0,0],"e":[242,242],"to":[40.3333320617676,0],"ti":[-40.3333320617676,0]},{"t":113.000004602584}],"ix":3},"r":{"a":0,"k":0,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":1,"lj":1,"ml":4,"ml2":{"a":0,"k":4,"ix":8},"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-492.189,-63.696],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-17.0000006924242,"op":496.000020202494,"st":0,"bm":0}],"markers":[]}
+
diff --git a/tests/manual/testApp/rect_gfill_linear.json b/tests/manual/testApp/rect_gfill_linear.json
new file mode 100644
index 0000000..f6993f1
--- /dev/null
+++ b/tests/manual/testApp/rect_gfill_linear.json
@@ -0,0 +1 @@
+{"v":"5.3.1","fr":29.9700012207031,"ip":0,"op":241.000009816131,"w":356,"h":200,"nm":"Comp 1","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[110.5,70.5,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,88.682,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[100,100],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":0,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":1,"lj":1,"ml":4,"ml2":{"a":0,"k":4,"ix":8},"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[2,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":0,"s":[0],"e":[191]},{"t":139.000005661586}],"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":2,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gf","o":{"a":0,"k":100,"ix":10},"r":1,"g":{"p":3,"k":{"a":0,"k":[0,1,0,0,0.5,0.5,0,0.5,1,0,0,1],"ix":9}},"s":{"a":0,"k":[-50,-50],"ix":5},"e":{"a":0,"k":[0,0],"ix":6},"t":1,"nm":"Gradient Fill 1","mn":"ADBE Vector Graphic - G-Fill","hd":false}],"ip":0,"op":175.000007127896,"st":0,"bm":0}],"markers":[]}
diff --git a/tests/manual/testApp/rect_gfill_radial.json b/tests/manual/testApp/rect_gfill_radial.json
new file mode 100644
index 0000000..51b291b
--- /dev/null
+++ b/tests/manual/testApp/rect_gfill_radial.json
@@ -0,0 +1 @@
+{"v":"5.3.1","fr":29.9700012207031,"ip":0,"op":241.000009816131,"w":356,"h":200,"nm":"Comp 1","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[100,100,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[100,100],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":0,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":1,"lj":1,"ml":4,"ml2":{"a":0,"k":4,"ix":8},"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":0,"s":[0],"e":[191]},{"t":139.000005661586}],"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":2,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gf","o":{"a":0,"k":100,"ix":10},"r":1,"g":{"p":3,"k":{"a":0,"k":[0,1,0,0,0.5,0.5,0,0.5,1,0,0,1],"ix":9}},"s":{"a":0,"k":[-50,-50],"ix":5},"e":{"a":0,"k":[0,0],"ix":6},"t":2,"h":{"a":0,"k":100,"ix":7},"a":{"a":0,"k":0,"ix":8},"nm":"Gradient Fill 1","mn":"ADBE Vector Graphic - G-Fill","hd":false}],"ip":0,"op":175.000007127896,"st":0,"bm":0}],"markers":[]}
diff --git a/tests/manual/testApp/rect_move_bezier.json b/tests/manual/testApp/rect_move_bezier.json
new file mode 100644
index 0000000..4e5e179
--- /dev/null
+++ b/tests/manual/testApp/rect_move_bezier.json
@@ -0,0 +1 @@
+{"v":"5.3.1","fr":29.9700012207031,"ip":0,"op":175.000007127896,"w":400,"h":224,"nm":"Comp 1","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":0,"s":[200,112,0],"e":[331,214,0],"to":[21.8333339691162,17,0],"ti":[-161.33332824707,2.33333325386047,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":35,"s":[331,214,0],"e":[493,89,0],"to":[161.33332824707,-2.33333325386047,0],"ti":[13.3333330154419,12.75,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":83,"s":[493,89,0],"e":[251,137.5,0],"to":[-13.3333330154419,-12.75,0],"ti":[40.3333320617676,-8.08333301544189,0]},{"t":122.000004969162}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[38,-32.5],[-38,-32.5],[-38,32.5],[38,32.5]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":1,"ix":5},"lc":1,"lj":1,"ml":4,"ml2":{"a":0,"k":4,"ix":8},"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.23529399797,1,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-137.5,-59],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":175.000007127896,"st":0,"bm":0}],"markers":[]}
diff --git a/tests/manual/testApp/rect_negative_scale_anchors.json b/tests/manual/testApp/rect_negative_scale_anchors.json
new file mode 100644
index 0000000..3baeb55
--- /dev/null
+++ b/tests/manual/testApp/rect_negative_scale_anchors.json
@@ -0,0 +1 @@
+{"v":"5.3.1","fr":29.9700012207031,"ip":0,"op":175.000007127896,"w":550,"h":309,"nm":"Comp 1","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[275,154.5,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0]],"o":[[0,0]],"v":[[-562,150.5]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":1,"lj":1,"ml":4,"ml2":{"a":0,"k":4,"ix":8},"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-272,1.5],[276,5.5]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":2,"ty":"sh","ix":3,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[1,-152.5],[-2,156.5]],"c":false},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":1,"lj":1,"ml":4,"ml2":{"a":0,"k":4,"ix":8},"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false}],"ip":0,"op":175.000007127896,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[275,154.5,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[-100,-100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[100,100],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":0,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[50,50],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":1,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4,"ix":5},"lc":1,"lj":1,"ml":4,"ml2":{"a":0,"k":4,"ix":8},"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false}],"ip":0,"op":175.000007127896,"st":0,"bm":0}],"markers":[]}
diff --git a/tests/manual/testApp/rect_negative_scale_pos.json b/tests/manual/testApp/rect_negative_scale_pos.json
new file mode 100644
index 0000000..e327813
--- /dev/null
+++ b/tests/manual/testApp/rect_negative_scale_pos.json
@@ -0,0 +1 @@
+{"v":"5.3.1","fr":29.9700012207031,"ip":0,"op":175.000007127896,"w":550,"h":309,"nm":"Comp 1","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[275,154.5,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0]],"o":[[0,0]],"v":[[-562,150.5]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":1,"lj":1,"ml":4,"ml2":{"a":0,"k":4,"ix":8},"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-272,1.5],[276,5.5]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":2,"ty":"sh","ix":3,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[1,-152.5],[-2,156.5]],"c":false},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":1,"lj":1,"ml":4,"ml2":{"a":0,"k":4,"ix":8},"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false}],"ip":0,"op":175.000007127896,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[275,154.5,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[-100,-100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[100,100],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":0,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"tr","p":{"a":0,"k":[50,50],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":1,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[50,50],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":1,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4,"ix":5},"lc":1,"lj":1,"ml":4,"ml2":{"a":0,"k":4,"ix":8},"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false}],"ip":0,"op":175.000007127896,"st":0,"bm":0}],"markers":[]}
diff --git a/tests/manual/testApp/rect_negative_scale_pos_anchors_rot.json b/tests/manual/testApp/rect_negative_scale_pos_anchors_rot.json
new file mode 100644
index 0000000..a88ca9d
--- /dev/null
+++ b/tests/manual/testApp/rect_negative_scale_pos_anchors_rot.json
@@ -0,0 +1,2 @@
+{"v":"5.3.1","fr":29.9700012207031,"ip":0,"op":175.000007127896,"w":550,"h":309,"nm":"Comp 1","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[275,154.5,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0]],"o":[[0,0]],"v":[[-562,150.5]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":1,"lj":1,"ml":4,"ml2":{"a":0,"k":4,"ix":8},"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-272,1.5],[276,5.5]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":2,"ty":"sh","ix":3,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[1,-152.5],[-2,156.5]],"c":false},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":1,"lj":1,"ml":4,"ml2":{"a":0,"k":4,"ix":8},"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false}],"ip":0,"op":175.000007127896,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":90,"ix":10},"p":{"a":0,"k":[275,154.5,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[-100,-100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[100,100],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":0,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"tr","p":{"a":0,"k":[50,50],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":1,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[50,50],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":1,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4,"ix":5},"lc":1,"lj":1,"ml":4,"ml2":{"a":0,"k":4,"ix":8},"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false}],"ip":0,"op":175.000007127896,"st":0,"bm":0}],"markers":[]}
+
diff --git a/tests/manual/testApp/rect_rotate.json b/tests/manual/testApp/rect_rotate.json
new file mode 100644
index 0000000..a049549
--- /dev/null
+++ b/tests/manual/testApp/rect_rotate.json
@@ -0,0 +1,2 @@
+{"v":"5.3.1","fr":60,"ip":0,"op":114.000004643315,"w":356,"h":200,"nm":"Comp 1","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":0,"s":[0],"e":[63]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":45,"s":[63],"e":[0]},{"t":90.0000036657751}],"ix":10},"p":{"a":0,"k":[178,100,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[54.5,66.5],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":0,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":1,"lj":1,"ml":4,"ml2":{"a":0,"k":4,"ix":8},"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":114.000004643315,"st":0,"bm":0}],"markers":[]}
+
diff --git a/tests/manual/testApp/rect_rotate_layer.json b/tests/manual/testApp/rect_rotate_layer.json
new file mode 100644
index 0000000..f270c31
--- /dev/null
+++ b/tests/manual/testApp/rect_rotate_layer.json
@@ -0,0 +1 @@
+{"v":"5.3.1","fr":29.9700012207031,"ip":0,"op":175.000007127896,"w":550,"h":309,"nm":"Comp 1","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":0,"s":[0],"e":[720]},{"t":70.0000028511585}],"ix":10},"p":{"a":0,"k":[275,154.5,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-6,99.5],[267,99.5]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4,"ix":5},"lc":1,"lj":1,"ml":4,"ml2":{"a":0,"k":4,"ix":8},"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[36,-32],[-36,-32],[-36,32],[36,32]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4,"ix":5},"lc":1,"lj":1,"ml":4,"ml2":{"a":0,"k":4,"ix":8},"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[50,50],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":3,"cix":2,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ind":2,"ty":"sh","ix":3,"ks":{"a":0,"k":{"i":[],"o":[],"v":[],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false}],"ip":0,"op":175.000007127896,"st":0,"bm":0}],"markers":[]}
diff --git a/tests/manual/testApp/rect_rotate_layer2.json b/tests/manual/testApp/rect_rotate_layer2.json
new file mode 100644
index 0000000..0b70115
--- /dev/null
+++ b/tests/manual/testApp/rect_rotate_layer2.json
@@ -0,0 +1 @@
+{"v":"5.3.1","fr":29.9700012207031,"ip":0,"op":175.000007127896,"w":550,"h":309,"nm":"Comp 1","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[275,154.5,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[-100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-6,99.5],[267,99.5]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4,"ix":5},"lc":1,"lj":1,"ml":4,"ml2":{"a":0,"k":4,"ix":8},"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[36,-32],[-36,-32],[-36,32],[36,32]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4,"ix":5},"lc":1,"lj":1,"ml":4,"ml2":{"a":0,"k":4,"ix":8},"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[87,59.5],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":3,"cix":2,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ind":2,"ty":"sh","ix":3,"ks":{"a":0,"k":{"i":[],"o":[],"v":[],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false}],"ip":0,"op":175.000007127896,"st":0,"bm":0}],"markers":[]}
diff --git a/tests/manual/testApp/rect_rotate_shape.json b/tests/manual/testApp/rect_rotate_shape.json
new file mode 100644
index 0000000..5465c36
--- /dev/null
+++ b/tests/manual/testApp/rect_rotate_shape.json
@@ -0,0 +1 @@
+{"v":"5.3.1","fr":29.9700012207031,"ip":0,"op":175.000007127896,"w":550,"h":309,"nm":"Comp 1","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[275,154.5,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[36,-32],[-36,-32],[-36,32],[36,32]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4,"ix":5},"lc":1,"lj":1,"ml":4,"ml2":{"a":0,"k":4,"ix":8},"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[87,59.5],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":0,"s":[360],"e":[720]},{"t":70.0000028511585}],"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":175.000007127896,"st":0,"bm":0}],"markers":[]}
diff --git a/tests/manual/testApp/rect_scale.json b/tests/manual/testApp/rect_scale.json
new file mode 100644
index 0000000..6f0559d
--- /dev/null
+++ b/tests/manual/testApp/rect_scale.json
@@ -0,0 +1 @@
+{"v":"5.3.1","fr":29.9700012207031,"ip":0,"op":175.000007127896,"w":400,"h":224,"nm":"rect_scale","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[200,112,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"n":["0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167"],"t":0,"s":[100,100,100],"e":[0,0,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"n":["0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167"],"t":47,"s":[0,0,100],"e":[100,100,100]},{"t":94.0000038286985}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[47,-41.5],[-47,-41.5],[-47,41.5],[47,41.5]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4,"ix":5},"lc":1,"lj":1,"ml":4,"ml2":{"a":0,"k":4,"ix":8},"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.23529399797,1,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[3,2.5],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":95.0000038694293,"st":0,"bm":0}],"markers":[]}
diff --git a/tests/manual/testApp/rect_scale_anchors_on_layer.json b/tests/manual/testApp/rect_scale_anchors_on_layer.json
new file mode 100644
index 0000000..7574a14
--- /dev/null
+++ b/tests/manual/testApp/rect_scale_anchors_on_layer.json
@@ -0,0 +1 @@
+{"v":"5.3.1","fr":30,"ip":30,"op":60,"w":320,"h":320,"nm":"Star - On","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Star Echo","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[128.5,77.5,0],"ix":2},"a":{"a":0,"k":[240,240,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.25,0.25,0.25],"y":[1,1,1]},"o":{"x":[0.001,0.001,0.001],"y":[0,0,0]},"n":["0p25_1_0p001_0","0p25_1_0p001_0","0p25_1_0p001_0"],"t":30,"s":[100,100,100],"e":[70,70,100]},{"i":{"x":[0,0,0],"y":[1,1,1]},"o":{"x":[0.001,0.001,0.001],"y":[0,0,0]},"n":["0_1_0p001_0","0_1_0p001_0","0_1_0p001_0"],"t":36,"s":[70,70,100],"e":[200,200,100]},{"t":56}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[8.843,-21.095],[-23.375,-21.095],[-23.375,13.122],[8.843,13.122]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.638999968884,0.513999968884,0.380000005984,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":3,"ix":5},"lc":1,"lj":1,"ml":4,"ml2":{"a":0,"k":4,"ix":8},"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[255.393,265.656],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":90,"st":15,"bm":0}],"markers":[{"tm":30,"cm":"Start","dr":0},{"tm":60,"cm":"End","dr":0}]}
diff --git a/tests/manual/testApp/rect_scale_anchors_on_rect.json b/tests/manual/testApp/rect_scale_anchors_on_rect.json
new file mode 100644
index 0000000..fb54438
--- /dev/null
+++ b/tests/manual/testApp/rect_scale_anchors_on_rect.json
@@ -0,0 +1 @@
+{"v":"5.3.1","fr":30,"ip":30,"op":60,"w":320,"h":320,"nm":"Star - On","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Star Echo","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[128.5,77.5,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.25,0.25,0.25],"y":[1,1,1]},"o":{"x":[0.001,0.001,0.001],"y":[0,0,0]},"n":["0p25_1_0p001_0","0p25_1_0p001_0","0p25_1_0p001_0"],"t":30,"s":[100,100,100],"e":[70,70,100]},{"i":{"x":[0,0,0],"y":[1,1,1]},"o":{"x":[0.001,0.001,0.001],"y":[0,0,0]},"n":["0_1_0p001_0","0_1_0p001_0","0_1_0p001_0"],"t":36,"s":[70,70,100],"e":[200,200,100]},{"t":56}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[8.843,-21.095],[-23.375,-21.095],[-23.375,13.122],[8.843,13.122]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.638999968884,0.513999968884,0.380000005984,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":3,"ix":5},"lc":1,"lj":1,"ml":4,"ml2":{"a":0,"k":4,"ix":8},"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[255.393,265.656],"ix":2},"a":{"a":0,"k":[240,240],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":90,"st":15,"bm":0}],"markers":[{"tm":30,"cm":"Start","dr":0},{"tm":60,"cm":"End","dr":0}]}
diff --git a/tests/manual/testApp/rect_skew.json b/tests/manual/testApp/rect_skew.json
new file mode 100644
index 0000000..5c89472
--- /dev/null
+++ b/tests/manual/testApp/rect_skew.json
@@ -0,0 +1 @@
+{"v":"5.3.1","fr":29.9700012207031,"ip":0,"op":241.000009816131,"w":356,"h":200,"nm":"Comp 1","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[106.5,71.5,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[43.5,43.5],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":0,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":1,"lj":1,"ml":4,"ml2":{"a":0,"k":4,"ix":8},"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":0,"s":[0],"e":[41]},{"t":139.000005661586}],"ix":4},"sa":{"a":0,"k":191,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":175.000007127896,"st":0,"bm":0}],"markers":[]}
diff --git a/tests/manual/testApp/rect_skew_axis.json b/tests/manual/testApp/rect_skew_axis.json
new file mode 100644
index 0000000..d31b1e6
--- /dev/null
+++ b/tests/manual/testApp/rect_skew_axis.json
@@ -0,0 +1 @@
+{"v":"5.3.1","fr":29.9700012207031,"ip":0,"op":241.000009816131,"w":356,"h":200,"nm":"Comp 1","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[106.5,71.5,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[43.5,43.5],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":0,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":1,"lj":1,"ml":4,"ml2":{"a":0,"k":4,"ix":8},"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":41,"ix":4},"sa":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":0,"s":[0],"e":[191]},{"t":139.000005661586}],"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":175.000007127896,"st":0,"bm":0}],"markers":[]}
diff --git a/tests/manual/testApp/repeater1.json b/tests/manual/testApp/repeater1.json
new file mode 100644
index 0000000..9361d4e
--- /dev/null
+++ b/tests/manual/testApp/repeater1.json
@@ -0,0 +1 @@
+{"v":"5.3.1","fr":29.9700012207031,"ip":0,"op":175.000007127896,"w":400,"h":224,"nm":"Comp 1","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[64,76,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"d":1,"ty":"el","s":{"a":0,"k":[100,100],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.615686297417,0.129411771894,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 2","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"rc","d":1,"s":{"a":0,"k":[100,100],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":0,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"gr","it":[{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":0,"cix":2,"ix":4,"mn":"ADBE Vector Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"rp","c":{"a":0,"k":3,"ix":1},"o":{"a":0,"k":0,"ix":2},"m":1,"ix":6,"tr":{"ty":"tr","p":{"a":0,"k":[100,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":4},"so":{"a":0,"k":100,"ix":5},"eo":{"a":0,"k":100,"ix":6},"nm":"Transform"},"nm":"Repeater 1","mn":"ADBE Vector Filter - Repeater","hd":false}],"ip":0,"op":175.000007127896,"st":0,"bm":0}],"markers":[]}
diff --git a/tests/manual/testApp/repeater_group1.json b/tests/manual/testApp/repeater_group1.json
new file mode 100644
index 0000000..3bddb7d
--- /dev/null
+++ b/tests/manual/testApp/repeater_group1.json
@@ -0,0 +1 @@
+{"v":"5.3.1","fr":29.9700012207031,"ip":0,"op":175.000007127896,"w":400,"h":224,"nm":"Comp 1","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[64,76,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[56,56],"ix":2},"p":{"a":0,"k":[0,86],"ix":3},"r":{"a":0,"k":0,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[0.070588238537,0.917647063732,0.980392158031,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false},{"d":1,"ty":"el","s":{"a":0,"k":[100,100],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.615686297417,0.129411771894,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 2","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"rc","d":1,"s":{"a":0,"k":[100,100],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":0,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"rp","c":{"a":0,"k":3,"ix":1},"o":{"a":0,"k":0,"ix":2},"m":1,"ix":6,"tr":{"ty":"tr","p":{"a":0,"k":[105,12],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[73,73],"ix":3},"r":{"a":0,"k":0,"ix":4},"so":{"a":0,"k":100,"ix":5},"eo":{"a":0,"k":0,"ix":6},"nm":"Transform"},"nm":"Repeater 1","mn":"ADBE Vector Filter - Repeater","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[6.25,-5.5],[-6.25,-5.5],[-6.25,5.5],[6.25,5.5]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":1,"ix":5},"lc":1,"lj":1,"ml":4,"ml2":{"a":0,"k":4,"ix":8},"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.23529399797,1,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-25.75,-66.5],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":3,"cix":2,"ix":7,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":175.000007127896,"st":0,"bm":0}],"markers":[]}
diff --git a/tests/manual/testApp/repeater_group2.json b/tests/manual/testApp/repeater_group2.json
new file mode 100644
index 0000000..9fcf3ec
--- /dev/null
+++ b/tests/manual/testApp/repeater_group2.json
@@ -0,0 +1 @@
+{"v":"5.3.1","fr":29.9700012207031,"ip":0,"op":175.000007127896,"w":550,"h":309,"nm":"Comp 1","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[28.5,-4,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"d":2,"ty":"el","s":{"a":0,"k":[100,100],"ix":2},"p":{"a":0,"k":[267,155],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[54,-44],[-54,-44],[-54,44],[54,44]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[190,154],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":2,"cix":2,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4,"ix":5},"lc":1,"lj":1,"ml":4,"ml2":{"a":0,"k":4,"ix":8},"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":0,"k":50,"ix":2},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":0,"s":[-25],"e":[75]},{"t":61.0000024845809}],"ix":3},"m":1,"ix":4,"nm":"Trim Paths 2","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"rp","c":{"a":0,"k":3,"ix":1},"o":{"a":0,"k":0,"ix":2},"m":1,"ix":6,"tr":{"ty":"tr","p":{"a":0,"k":[124,73],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[89,89],"ix":3},"r":{"a":0,"k":0,"ix":4},"so":{"a":0,"k":100,"ix":5},"eo":{"a":0,"k":100,"ix":6},"nm":"Transform"},"nm":"Repeater 1","mn":"ADBE Vector Filter - Repeater","hd":false}],"ip":0,"op":175.000007127896,"st":0,"bm":0}],"markers":[]}
diff --git a/tests/manual/testApp/shape_bezier_simple.json b/tests/manual/testApp/shape_bezier_simple.json
new file mode 100644
index 0000000..d002cfd
--- /dev/null
+++ b/tests/manual/testApp/shape_bezier_simple.json
@@ -0,0 +1 @@
+{"v":"5.3.1","fr":29.9700012207031,"ip":0,"op":175.000007127896,"w":400,"h":224,"nm":"Comp 1","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[200,112,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[-15,0]],"o":[[0,0],[58.502,0]],"v":[[1,0],[-0.5,61]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":1,"lj":1,"ml":4,"ml2":{"a":0,"k":4,"ix":8},"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":1,"lj":1,"ml":4,"ml2":{"a":0,"k":4,"ix":8},"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false}],"ip":0,"op":174.825007120768,"st":0,"bm":0}],"markers":[]}
diff --git a/tests/manual/testApp/shape_circle.json b/tests/manual/testApp/shape_circle.json
new file mode 100644
index 0000000..bd41883
--- /dev/null
+++ b/tests/manual/testApp/shape_circle.json
@@ -0,0 +1 @@
+{"v":"5.3.1","fr":29.9700012207031,"ip":0,"op":175.000007127896,"w":356,"h":200,"nm":"Comp 1","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[178,100,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[47.497,-0.511],[-43,0]],"o":[[-46.5,0.5],[50,0]],"v":[[0.5,0.5],[0,69.5]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":1,"lj":1,"ml":4,"ml2":{"a":0,"k":4,"ix":8},"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":175.000007127896,"st":0,"bm":0}],"markers":[]}
diff --git a/tests/manual/testApp/shape_complex.json b/tests/manual/testApp/shape_complex.json
new file mode 100644
index 0000000..f726168
--- /dev/null
+++ b/tests/manual/testApp/shape_complex.json
@@ -0,0 +1 @@
+{"v":"5.3.1","fr":29.9700012207031,"ip":0,"op":175.000007127896,"w":400,"h":224,"nm":"Comp 1","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[200,112,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[-38.234,34.801],[-32.222,-9.403],[-14.71,0.126],[-21.203,33.16]],"o":[[0,0],[4.82,-4.387],[32.222,9.403],[23.332,-0.199],[21.203,-33.16]],"v":[[81,22],[10.234,8.699],[1.278,-25.403],[2.5,-59],[55.297,-30.84]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":1,"lj":1,"ml":4,"ml2":{"a":0,"k":4,"ix":8},"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":1,"lj":1,"ml":4,"ml2":{"a":0,"k":4,"ix":8},"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false}],"ip":0,"op":174.825007120768,"st":0,"bm":0}],"markers":[]}
diff --git a/tests/manual/testApp/shape_complex2.json b/tests/manual/testApp/shape_complex2.json
new file mode 100644
index 0000000..554dd1f
--- /dev/null
+++ b/tests/manual/testApp/shape_complex2.json
@@ -0,0 +1 @@
+{"v":"5.3.1","fr":29.9700012207031,"ip":0,"op":175.000007127896,"w":356,"h":200,"nm":"Comp 1","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[178,100,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-13,-0.5],[0,0],[12.927,19.168],[0,0]],"o":[[47.465,1.826],[0,0],[-14.5,-21.5],[0,0]],"v":[[3.5,4],[17.5,39.5],[-28.5,29.5],[21,-23]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":1,"lj":1,"ml":4,"ml2":{"a":0,"k":4,"ix":8},"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":175.000007127896,"st":0,"bm":0}],"markers":[]}
diff --git a/tests/manual/testApp/shape_static.json b/tests/manual/testApp/shape_static.json
new file mode 100644
index 0000000..6a88e15
--- /dev/null
+++ b/tests/manual/testApp/shape_static.json
@@ -0,0 +1 @@
+{"v":"5.3.1","fr":29.9700012207031,"ip":0,"op":175.000007127896,"w":356,"h":200,"nm":"Comp 1","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[178,100,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[35.368,-37.448],[-9,37.5]],"o":[[0,0],[0,0],[-34,36],[9,-37.5]],"v":[[-16,-29],[-45,-21.5],[-100.5,14.5],[56,3]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":1,"lj":1,"ml":4,"ml2":{"a":0,"k":4,"ix":8},"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":175.000007127896,"st":0,"bm":0}],"markers":[]}
diff --git a/tests/manual/testApp/stroke.json b/tests/manual/testApp/stroke.json
new file mode 100644
index 0000000..0248085
--- /dev/null
+++ b/tests/manual/testApp/stroke.json
@@ -0,0 +1 @@
+{"v":"5.3.1","fr":29.9700012207031,"ip":0,"op":175.000007127896,"w":400,"h":224,"nm":"Comp 1","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[200,112,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"d":1,"ty":"el","s":{"a":0,"k":[100,100],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"st","c":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":0,"s":[0,0,0,1],"e":[1,1,1,1]},{"t":64.0000026067734}],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":1,"lj":1,"ml":4,"ml2":{"a":0,"k":4,"ix":8},"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false}],"ip":0,"op":175.000007127896,"st":0,"bm":0}],"markers":[]}
diff --git a/tests/manual/testApp/testApp.pro b/tests/manual/testApp/testApp.pro
new file mode 100644
index 0000000..621f618
--- /dev/null
+++ b/tests/manual/testApp/testApp.pro
@@ -0,0 +1,34 @@
+QT += quick
+CONFIG += c++11
+
+# The following define makes your compiler emit warnings if you use
+# any feature of Qt which as been marked deprecated (the exact warnings
+# depend on your compiler). Please consult the documentation of the
+# deprecated API in order to know how to port your code away from it.
+DEFINES += QT_DEPRECATED_WARNINGS
+
+# You can also make your code fail to compile if you use deprecated APIs.
+# In order to do so, uncomment the following line.
+# You can also select to disable deprecated APIs only up to a certain version of Qt.
+#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0
+
+SOURCES += \
+ main.cpp \
+
+RESOURCES += qml.qrc
+
+# Additional import path used to resolve QML modules in Qt Creator's code model
+QML_IMPORT_PATH =
+# Additional import path used to resolve QML modules just for Qt Quick Designer
+QML_DESIGNER_IMPORT_PATH =
+
+# Default rules for deployment.
+qnx: target.path = /tmp/$${TARGET}/bin
+else: unix:!android: target.path = /opt/$${TARGET}/bin
+!isEmpty(target.path): INSTALLS += target
+
+qmldir.files = C:\users\kahautam\projects\lg\lottie-qt\src\imports\qmldir
+qmldir.path = /usr/lib/qml/Qt/labs/lottieqt
+INSTALLS += qmldir
+
+#HEADERS +=
diff --git a/tests/manual/testApp/trim_path1.json b/tests/manual/testApp/trim_path1.json
new file mode 100644
index 0000000..872c971
--- /dev/null
+++ b/tests/manual/testApp/trim_path1.json
@@ -0,0 +1 @@
+{"v":"5.3.1","fr":29.9700012207031,"ip":0,"op":175.000007127896,"w":550,"h":309,"nm":"Comp 1","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[50,50,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"rc","d":1,"s":{"a":0,"k":[100,100],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":0,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":0,"s":[0],"e":[100]},{"t":50.0000020365418}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":2,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":16,"ix":5},"lc":1,"lj":1,"ml":4,"ml2":{"a":0,"k":4,"ix":8},"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false}],"ip":0,"op":175.000007127896,"st":0,"bm":0}],"markers":[]}
diff --git a/tests/manual/testApp/trim_path2.json b/tests/manual/testApp/trim_path2.json
new file mode 100644
index 0000000..8bb22d5
--- /dev/null
+++ b/tests/manual/testApp/trim_path2.json
@@ -0,0 +1 @@
+{"v":"5.3.1","fr":29.9700012207031,"ip":0,"op":175.000007127896,"w":550,"h":309,"nm":"Comp 1","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,0,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[54,-44],[-54,-44],[-54,44],[54,44]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":16,"ix":5},"lc":1,"lj":1,"ml":4,"ml2":{"a":0,"k":4,"ix":8},"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[103,158],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false},{"d":2,"ty":"el","s":{"a":0,"k":[100,100],"ix":2},"p":{"a":0,"k":[267,155],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":0,"s":[0],"e":[100]},{"t":50.0000020365418}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":3,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":16,"ix":5},"lc":1,"lj":1,"ml":4,"ml2":{"a":0,"k":4,"ix":8},"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false}],"ip":0,"op":175.000007127896,"st":0,"bm":0}],"markers":[]}
diff --git a/tests/manual/testApp/trim_path2_individual.json b/tests/manual/testApp/trim_path2_individual.json
new file mode 100644
index 0000000..bf88c01
--- /dev/null
+++ b/tests/manual/testApp/trim_path2_individual.json
@@ -0,0 +1 @@
+{"v":"5.3.1","fr":29.9700012207031,"ip":0,"op":175.000007127896,"w":550,"h":309,"nm":"Comp 1","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[9,-2,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"d":2,"ty":"el","s":{"a":0,"k":[100,100],"ix":2},"p":{"a":0,"k":[267,155],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[54,-44],[-54,-44],[-54,44],[54,44]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[190,154],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":2,"cix":2,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":16,"ix":5},"lc":1,"lj":1,"ml":4,"ml2":{"a":0,"k":4,"ix":8},"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":0,"s":[0],"e":[100]},{"t":50.0000020365418}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":2,"ix":4,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false}],"ip":0,"op":175.000007127896,"st":0,"bm":0}],"markers":[]}
diff --git a/tests/manual/testApp/trim_path2_offset.json b/tests/manual/testApp/trim_path2_offset.json
new file mode 100644
index 0000000..36e8804
--- /dev/null
+++ b/tests/manual/testApp/trim_path2_offset.json
@@ -0,0 +1 @@
+{"v":"5.3.1","fr":29.9700012207031,"ip":0,"op":175.000007127896,"w":550,"h":309,"nm":"Comp 1","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[28.5,-4,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"d":2,"ty":"el","s":{"a":0,"k":[100,100],"ix":2},"p":{"a":0,"k":[267,155],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[54,-44],[-54,-44],[-54,44],[54,44]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[190,154],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":2,"cix":2,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4,"ix":5},"lc":1,"lj":1,"ml":4,"ml2":{"a":0,"k":4,"ix":8},"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":0,"k":50,"ix":2},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":0,"s":[-25],"e":[75]},{"t":61.0000024845809}],"ix":3},"m":1,"ix":4,"nm":"Trim Paths 2","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false}],"ip":0,"op":175.000007127896,"st":0,"bm":0}],"markers":[]}
diff --git a/tests/manual/testApp/trim_path_multiple1.json b/tests/manual/testApp/trim_path_multiple1.json
new file mode 100644
index 0000000..113e8c5
--- /dev/null
+++ b/tests/manual/testApp/trim_path_multiple1.json
@@ -0,0 +1 @@
+{"v":"5.3.1","fr":29.9700012207031,"ip":0,"op":175.000007127896,"w":550,"h":309,"nm":"Comp 1","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[28.5,-4,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"d":2,"ty":"el","s":{"a":0,"k":[100,100],"ix":2},"p":{"a":0,"k":[267,155],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[54,-44],[-54,-44],[-54,44],[54,44]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[190,154],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":2,"cix":2,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4,"ix":5},"lc":1,"lj":1,"ml":4,"ml2":{"a":0,"k":4,"ix":8},"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tm","s":{"a":0,"k":50,"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":0,"s":[100],"e":[100]},{"t":53.0000021587343}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":4,"nm":"Trim Paths 2","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":0,"s":[0],"e":[100]},{"t":53.0000021587343}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":5,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false}],"ip":0,"op":175.000007127896,"st":0,"bm":0}],"markers":[]}
diff --git a/tests/manual/testApp/trim_path_multiple2.json b/tests/manual/testApp/trim_path_multiple2.json
new file mode 100644
index 0000000..4ca4696
--- /dev/null
+++ b/tests/manual/testApp/trim_path_multiple2.json
@@ -0,0 +1 @@
+{"v":"5.3.1","fr":29.9700012207031,"ip":0,"op":175.000007127896,"w":550,"h":309,"nm":"Comp 1","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[28.5,-4,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"d":2,"ty":"el","s":{"a":0,"k":[100,100],"ix":2},"p":{"a":0,"k":[267,155],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[54,-44],[-54,-44],[-54,44],[54,44]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[190,154],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":2,"cix":2,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4,"ix":5},"lc":1,"lj":1,"ml":4,"ml2":{"a":0,"k":4,"ix":8},"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tm","s":{"a":0,"k":25,"ix":1},"e":{"a":0,"k":75,"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":4,"nm":"Trim Paths 3","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"tm","s":{"a":0,"k":50,"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":0,"s":[100],"e":[100]},{"t":53.0000021587343}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":5,"nm":"Trim Paths 2","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":0,"s":[0],"e":[100]},{"t":53.0000021587343}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":6,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false}],"ip":0,"op":175.000007127896,"st":0,"bm":0}],"markers":[]}
diff --git a/tests/manual/testApp/trim_path_multiple3.json b/tests/manual/testApp/trim_path_multiple3.json
new file mode 100644
index 0000000..598585d
--- /dev/null
+++ b/tests/manual/testApp/trim_path_multiple3.json
@@ -0,0 +1 @@
+{"v":"5.3.1","fr":29.9700012207031,"ip":0,"op":175.000007127896,"w":550,"h":309,"nm":"Comp 1","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[28.5,-4,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"d":2,"ty":"el","s":{"a":0,"k":[100,100],"ix":2},"p":{"a":0,"k":[267,155],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[54,-44],[-54,-44],[-54,44],[54,44]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[190,154],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":2,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":0,"k":25,"ix":1},"e":{"a":0,"k":75,"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 3","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4,"ix":5},"lc":1,"lj":1,"ml":4,"ml2":{"a":0,"k":4,"ix":8},"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tm","s":{"a":0,"k":50,"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":0,"s":[100],"e":[100]},{"t":53.0000021587343}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":4,"nm":"Trim Paths 2","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":0,"s":[0],"e":[100]},{"t":53.0000021587343}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":5,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false}],"ip":0,"op":175.000007127896,"st":0,"bm":0}],"markers":[]}
diff --git a/tests/manual/testApp/trim_path_multiple4.json b/tests/manual/testApp/trim_path_multiple4.json
new file mode 100644
index 0000000..c3b6946
--- /dev/null
+++ b/tests/manual/testApp/trim_path_multiple4.json
@@ -0,0 +1 @@
+{"v":"5.3.1","fr":29.9700012207031,"ip":0,"op":175.000007127896,"w":550,"h":309,"nm":"Comp 1","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[28.5,-4,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"d":2,"ty":"el","s":{"a":0,"k":[100,100],"ix":2},"p":{"a":0,"k":[267,155],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":0,"s":[0],"e":[100]},{"t":53.0000021587343}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1.1","np":2,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false,"cl":"1"},{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[54,-44],[-54,-44],[-54,44],[54,44]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[190,154],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":2,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":0,"s":[0],"e":[100]},{"t":53.0000021587343}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":2,"ix":2,"nm":"Trim Paths 2","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1.2","np":2,"cix":2,"ix":2,"mn":"ADBE Vector Group","hd":false,"cl":"2"},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4,"ix":5},"lc":1,"lj":1,"ml":4,"ml2":{"a":0,"k":4,"ix":8},"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false}],"ip":0,"op":175.000007127896,"st":0,"bm":0}],"markers":[]}
diff --git a/tests/manual/testApp/trim_path_simultaneous2.json b/tests/manual/testApp/trim_path_simultaneous2.json
new file mode 100644
index 0000000..5618821
--- /dev/null
+++ b/tests/manual/testApp/trim_path_simultaneous2.json
@@ -0,0 +1 @@
+{"v":"5.3.1","fr":29.9700012207031,"ip":0,"op":175.000007127896,"w":400,"h":224,"nm":"Comp 1","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[200,112,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[-5,5.5],[-58,-33],[-118.5,6]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":0,"s":[50],"e":[0]},{"t":44.0000017921567}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":0,"s":[50],"e":[100]},{"t":44.0000017921567}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4,"ix":5},"lc":1,"lj":1,"ml":4,"ml2":{"a":0,"k":4,"ix":8},"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false}],"ip":0,"op":175.000007127896,"st":0,"bm":0}],"markers":[]}
diff --git a/tests/tests.pro b/tests/tests.pro
new file mode 100644
index 0000000..b71cdd5
--- /dev/null
+++ b/tests/tests.pro
@@ -0,0 +1,4 @@
+TEMPLATE = subdirs
+SUBDIRS += \
+ auto \
+ manual