From 18dfa7b7ed8c4243fc40397cc0a5e3001c7aa4f3 Mon Sep 17 00:00:00 2001 From: lhdjply Date: Tue, 28 May 2024 10:21:39 +0800 Subject: [PATCH] feat(thorvg): update thorvg version to 0.13.5 (#6274) Signed-off-by: lhdjply --- src/libs/thorvg/config.h | 5 +- src/libs/thorvg/thorvg.h | 115 +- src/libs/thorvg/thorvg_capi.h | 141 +- src/libs/thorvg/thorvg_lottie.h | 48 +- src/libs/thorvg/tvgAnimation.cpp | 27 + src/libs/thorvg/tvgArray.h | 1 + src/libs/thorvg/tvgCanvas.cpp | 6 + src/libs/thorvg/tvgCanvas.h | 63 +- src/libs/thorvg/tvgCapi.cpp | 58 +- src/libs/thorvg/tvgCompressor.cpp | 4 +- src/libs/thorvg/tvgFrameModule.h | 15 + src/libs/thorvg/tvgGlCanvas.cpp | 6 +- .../thorvg/{tvgBezier.cpp => tvgLines.cpp} | 27 +- src/libs/thorvg/{tvgBezier.h => tvgLines.h} | 17 +- src/libs/thorvg/tvgLoader.cpp | 50 +- src/libs/thorvg/tvgLottieAnimation.cpp | 36 + src/libs/thorvg/tvgLottieBuilder.cpp | 516 ++++--- src/libs/thorvg/tvgLottieBuilder.h | 14 +- src/libs/thorvg/tvgLottieExpressions.cpp | 1298 +++++++++++++++++ src/libs/thorvg/tvgLottieExpressions.h | 171 +++ src/libs/thorvg/tvgLottieInterpolator.cpp | 10 +- src/libs/thorvg/tvgLottieLoader.cpp | 165 ++- src/libs/thorvg/tvgLottieLoader.h | 8 + src/libs/thorvg/tvgLottieModel.cpp | 244 +++- src/libs/thorvg/tvgLottieModel.h | 277 ++-- src/libs/thorvg/tvgLottieParser.cpp | 552 ++++--- src/libs/thorvg/tvgLottieParser.h | 14 +- src/libs/thorvg/tvgLottieParserHandler.h | 4 +- src/libs/thorvg/tvgLottieProperty.h | 502 +++++-- src/libs/thorvg/tvgMath.cpp | 2 +- src/libs/thorvg/tvgMath.h | 33 +- src/libs/thorvg/tvgPaint.cpp | 27 +- src/libs/thorvg/tvgPicture.cpp | 9 +- src/libs/thorvg/tvgRender.cpp | 32 + src/libs/thorvg/tvgRender.h | 35 +- src/libs/thorvg/tvgSaver.cpp | 11 +- src/libs/thorvg/tvgScene.h | 1 - src/libs/thorvg/tvgShape.cpp | 12 +- src/libs/thorvg/tvgShape.h | 29 +- src/libs/thorvg/tvgStr.cpp | 3 + src/libs/thorvg/tvgSvgCssStyle.cpp | 16 + src/libs/thorvg/tvgSvgLoader.cpp | 255 +++- src/libs/thorvg/tvgSvgLoader.h | 2 +- src/libs/thorvg/tvgSvgLoaderCommon.h | 7 +- src/libs/thorvg/tvgSvgPath.cpp | 29 +- src/libs/thorvg/tvgSvgSceneBuilder.cpp | 8 +- src/libs/thorvg/tvgSvgUtil.cpp | 4 +- src/libs/thorvg/tvgSwCanvas.cpp | 2 + src/libs/thorvg/tvgSwFill.cpp | 4 +- src/libs/thorvg/tvgSwRaster.cpp | 13 +- src/libs/thorvg/tvgSwRasterAvx.h | 19 +- src/libs/thorvg/tvgSwRasterNeon.h | 60 +- src/libs/thorvg/tvgSwRenderer.cpp | 10 +- src/libs/thorvg/tvgSwRenderer.h | 1 + src/libs/thorvg/tvgSwShape.cpp | 129 +- src/libs/thorvg/tvgWgCanvas.cpp | 2 + src/libs/thorvg/tvgXmlParser.cpp | 5 +- src/libs/thorvg/tvgXmlParser.h | 6 +- tests/ref_imgs/widgets/lottie_2.png | Bin 4259 -> 4250 bytes tests/ref_imgs/widgets/lottie_2_small.png | Bin 3230 -> 3240 bytes tests/ref_imgs/widgets/lottie_3.png | Bin 6779 -> 6771 bytes tests/ref_imgs/widgets/lottie_3_small.png | Bin 4423 -> 4411 bytes tests/ref_imgs/widgets/lottie_4.png | Bin 7285 -> 7294 bytes 63 files changed, 4071 insertions(+), 1089 deletions(-) rename src/libs/thorvg/{tvgBezier.cpp => tvgLines.cpp} (92%) rename src/libs/thorvg/{tvgBezier.h => tvgLines.h} (88%) create mode 100644 src/libs/thorvg/tvgLottieExpressions.cpp create mode 100644 src/libs/thorvg/tvgLottieExpressions.h diff --git a/src/libs/thorvg/config.h b/src/libs/thorvg/config.h index 7d55247be..baa258a4f 100644 --- a/src/libs/thorvg/config.h +++ b/src/libs/thorvg/config.h @@ -5,12 +5,11 @@ #pragma once -#define THORVG_CAPI_BINDING_SUPPORT 1 - #define THORVG_SW_RASTER_SUPPORT 1 #define THORVG_SVG_LOADER_SUPPORT LV_USE_LOTTIE + #define THORVG_LOTTIE_LOADER_SUPPORT LV_USE_LOTTIE -#define THORVG_VERSION_STRING "0.11.99" +#define THORVG_VERSION_STRING "0.13.5" diff --git a/src/libs/thorvg/thorvg.h b/src/libs/thorvg/thorvg.h index 9a7e7ecc1..e591c662a 100644 --- a/src/libs/thorvg/thorvg.h +++ b/src/libs/thorvg/thorvg.h @@ -1,16 +1,3 @@ -/*! - * @file thorvg.h - * - * The main APIs enabling the TVG initialization, preparation of the canvas and provisioning of its content: - * - drawing shapes: line, arc, curve, path, polygon... - * - drawing pictures: tvg, svg, png, jpg, bitmap... - * - drawing fillings: solid, linear and radial gradient... - * - drawing stroking: continuous stroking with arbitrary width, join, cap, dash styles. - * - drawing composition: blending, masking, path clipping... - * - drawing scene graph & affine transformation (translation, rotation, scale, ...) - * and finally drawing the canvas and TVG termination. - */ - #ifndef _THORVG_H_ #define _THORVG_H_ @@ -673,6 +660,30 @@ public: */ virtual Result draw() noexcept; + /** + * @brief Sets the drawing region in the canvas. + * + * This function defines the rectangular area of the canvas that will be used for drawing operations. + * The specified viewport is used to clip the rendering output to the boundaries of the rectangle. + * + * @param[in] x The x-coordinate of the upper-left corner of the rectangle. + * @param[in] y The y-coordinate of the upper-left corner of the rectangle. + * @param[in] w The width of the rectangle. + * @param[in] h The height of the rectangle. + * + * @retval Result::Success when succeed, Result::InsufficientCondition otherwise. + * + * @see SwCanvas::target() + * @see GlCanvas::target() + * @see WgCanvas::target() + * + * @warning It's not allowed to change the viewport during Canvas::push() - Canvas::sync() or Canvas::update() - Canvas::sync(). + * + * @note When resetting the target, the viewport will also be reset to the target size. + * @note Experimental API + */ + virtual Result viewport(int32_t x, int32_t y, int32_t w, int32_t h) noexcept; + /** * @brief Guarantees that drawing task is finished. * @@ -1051,7 +1062,7 @@ public: * @param[in] miterlimit The miterlimit imposes a limit on the extent of the stroke join, when the @c StrokeJoin::Miter join style is set. The default value is 4. * * @retval Result::Success when succeed, Result::NonSupport unsupported value, Result::FailedAllocation otherwise. - * + * * @since 0.11 */ Result strokeMiterlimit(float miterlimit) noexcept; @@ -1669,7 +1680,7 @@ public: }; /** - * @brief Sets the target buffer for the rasterization. + * @brief Sets the drawing target for the rasterization. * * The buffer of a desirable size should be allocated and owned by the caller. * @@ -1684,7 +1695,9 @@ public: * @retval Result::InvalidArguments In case no valid pointer is provided or the width, or the height or the stride is zero. * @retval Result::NonSupport In case the software engine is not supported. * - * @warning Do not access @p buffer during Canvas::draw() - Canvas::sync(). It should not be accessed while TVG is writing on it. + * @warning Do not access @p buffer during Canvas::push() - Canvas::sync(). It should not be accessed while the engine is writing on it. + * + * @see Canvas::viewport() */ Result target(uint32_t* buffer, uint32_t stride, uint32_t w, uint32_t h, Colorspace cs) noexcept; @@ -1738,13 +1751,24 @@ public: ~GlCanvas(); /** - * @brief Sets the target buffer for the rasterization. + * @brief Sets the drawing target for rasterization. * - * @warning Please do not use it, this API is not official one. It could be modified in the next version. + * This function specifies the drawing target where the rasterization will occur. It can target + * a specific framebuffer object (FBO) or the main surface. * + * @param[in] id The GL target ID, usually indicating the FBO ID. A value of @c 0 specifies the main surface. + * @param[in] w The width (in pixels) of the raster image. + * @param[in] h The height (in pixels) of the raster image. + * + * @warning This API is experimental and not officially supported. It may be modified or removed in future versions. + * @warning Drawing on the main surface is currently not permitted. If the identifier (@p id) is set to @c 0, the operation will be aborted. + * + * @see Canvas::viewport() + * + * @note Currently, this only allows the GL_RGBA8 color space format. * @note Experimental API - */ - Result target(uint32_t* buffer, uint32_t stride, uint32_t w, uint32_t h) noexcept; + */ + Result target(int32_t id, uint32_t w, uint32_t h) noexcept; /** * @brief Creates a new GlCanvas object. @@ -1779,6 +1803,7 @@ public: * @warning Please do not use it, this API is not official one. It could be modified in the next version. * * @note Experimental API + * @see Canvas::viewport() */ Result target(void* window, uint32_t w, uint32_t h) noexcept; @@ -1852,7 +1877,7 @@ public: * * This class supports the display and control of animation frames. * - * @note Experimental API + * @since 0.13 */ class TVG_API Animation @@ -1869,9 +1894,12 @@ public: * @retval Result::InsufficientCondition if the given @p no is the same as the current frame value. * @retval Result::NonSupport The current Picture data does not support animations. * + * @note For efficiency, ThorVG ignores updates to the new frame value if the difference from the current frame value + * is less than 0.001. In such cases, it returns @c Result::InsufficientCondition. + * Values less than 0.001 may be disregarded and may not be accurately retained by the Animation. + * * @see totalFrame() * - * @note Experimental API */ Result frame(float no) noexcept; @@ -1886,7 +1914,6 @@ public: * * @warning The picture instance is owned by Animation. It should not be deleted manually. * - * @note Experimental API */ Picture* picture() const noexcept; @@ -1900,7 +1927,6 @@ public: * @see Animation::frame(float no) * @see Animation::totalFrame() * - * @note Experimental API */ float curFrame() const noexcept; @@ -1912,7 +1938,6 @@ public: * @note Frame numbering starts from 0. * @note If the Picture is not properly configured, this function will return 0. * - * @note Experimental API */ float totalFrame() const noexcept; @@ -1923,16 +1948,52 @@ public: * * @note If the Picture is not properly configured, this function will return 0. * - * @% Experimental API */ float duration() const noexcept; + /** + * @brief Specifies the playback segment of the animation. + * + * The set segment is designated as the play area of the animation. + * This is useful for playing a specific segment within the entire animation. + * After setting, the number of animation frames and the playback time are calculated + * by mapping the playback segment as the entire range. + * + * @param[in] begin segment start. + * @param[in] end segment end. + * + * @retval Result::Success When succeed. + * @retval Result::InsufficientCondition In case the animation is not loaded. + * @retval Result::InvalidArguments When the given parameter is invalid. + * @retval Result::NonSupport When it's not animatable. + * + * @note Range from 0.0~1.0 + * @note If a marker has been specified, its range will be disregarded. + * @see LottieAnimation::segment(const char* marker) + * @note Experimental API + */ + Result segment(float begin, float end) noexcept; + + /** + * @brief Gets the current segment. + * + * @param[out] begin segment start. + * @param[out] end segment end. + * + * @retval Result::Success When succeed. + * @retval Result::InsufficientCondition In case the animation is not loaded. + * @retval Result::InvalidArguments When the given parameter is invalid. + * @retval Result::NonSupport When it's not animatable. + * + * @note Experimental API + */ + Result segment(float* begin, float* end = nullptr) noexcept; + /** * @brief Creates a new Animation object. * * @return A new Animation object. * - * @note Experimental API */ static std::unique_ptr gen() noexcept; diff --git a/src/libs/thorvg/thorvg_capi.h b/src/libs/thorvg/thorvg_capi.h index 4aa3294a1..e71452274 100644 --- a/src/libs/thorvg/thorvg_capi.h +++ b/src/libs/thorvg/thorvg_capi.h @@ -103,7 +103,7 @@ typedef struct _Tvg_Gradient Tvg_Gradient; typedef struct _Tvg_Saver Tvg_Saver; /** -* \brief A structure representing an animation controller object. (Experimental API) +* \brief A structure representing an animation controller object. */ typedef struct _Tvg_Animation Tvg_Animation; @@ -404,8 +404,10 @@ typedef enum { * \brief Enumeration specifying the methods of combining the 8-bit color channels into 32-bit color. */ typedef enum { - TVG_COLORSPACE_ABGR8888 = 0, ///< The 8-bit color channels are combined into 32-bit color in the order: alpha, blue, green, red. - TVG_COLORSPACE_ARGB8888 ///< The 8-bit color channels are combined into 32-bit color in the order: alpha, red, green, blue. + TVG_COLORSPACE_ABGR8888 = 0, ///< The channels are joined in the order: alpha, blue, green, red. Colors are alpha-premultiplied. (a << 24 | b << 16 | g << 8 | r) + TVG_COLORSPACE_ARGB8888, ///< The channels are joined in the order: alpha, red, green, blue. Colors are alpha-premultiplied. (a << 24 | r << 16 | g << 8 | b) + TVG_COLORSPACE_ABGR8888S, ///< The channels are joined in the order: alpha, blue, green, red. Colors are un-alpha-premultiplied. @since 0.13 + TVG_COLORSPACE_ARGB8888S ///< The channels are joined in the order: alpha, red, green, blue. Colors are un-alpha-premultiplied. @since 0.13 } Tvg_Colorspace; @@ -457,7 +459,7 @@ TVG_API Tvg_Canvas* tvg_swcanvas_create(void); * \retval TVG_RESULT_INVALID_ARGUMENTS An invalid canvas or buffer pointer passed or one of the @p stride, @p w or @p h being zero. * \retval TVG_RESULT_NOT_SUPPORTED The software engine is not supported. * -* \warning Do not access @p buffer during tvg_canvas_draw() - tvg_canvas_sync(). It should not be accessed while TVG is writing on it. +* \warning Do not access @p buffer during tvg_canvas_draw() - tvg_canvas_sync(). It should not be accessed while the engine is writing on it. * * \see Tvg_Colorspace */ @@ -747,6 +749,30 @@ TVG_API Tvg_Result tvg_canvas_draw(Tvg_Canvas* canvas); TVG_API Tvg_Result tvg_canvas_sync(Tvg_Canvas* canvas); +/*! +* \brief Sets the drawing region in the canvas. +* +* This function defines the rectangular area of the canvas that will be used for drawing operations. +* The specified viewport is used to clip the rendering output to the boundaries of the rectangle. +* +* \param[in] canvas The Tvg_Canvas object containing elements which were drawn. +* \param[in] x The x-coordinate of the upper-left corner of the rectangle. +* \param[in] y The y-coordinate of the upper-left corner of the rectangle. +* \param[in] w The width of the rectangle. +* \param[in] h The height of the rectangle. +* +* \return Tvg_Result enumeration. +* \retval TVG_RESULT_SUCCESS Succeed. +* \retval TVG_RESULT_INSUFFICIENT_CONDITION An internal error. +* +* \warning It's not allowed to change the viewport during tvg_canvas_update() - tvg_canvas_sync() or tvg_canvas_push() - tvg_canvas_sync(). +* +* \note When resetting the target, the viewport will also be reset to the target size. +* \note Experimental API +* \see tvg_swcanvas_set_target() +*/ +TVG_API Tvg_Result tvg_canvas_set_viewport(Tvg_Canvas* canvas, int32_t x, int32_t y, int32_t w, int32_t h); + /** \} */ // end defgroup ThorVGCapi_Canvas @@ -2238,15 +2264,17 @@ TVG_API Tvg_Result tvg_saver_del(Tvg_Saver* saver); /************************************************************************/ /*! -* \brief Creates a new Animation object. (Experimental API) +* \brief Creates a new Animation object. * * \return Tvg_Animation A new Tvg_Animation object. +* +* \since 0.13 */ TVG_API Tvg_Animation* tvg_animation_new(void); /*! -* \brief Specifies the current frame in the animation. (Experimental API) +* \brief Specifies the current frame in the animation. * * \param[in] animation A Tvg_Animation pointer to the animation object. * \param[in] no The index of the animation frame to be displayed. The index should be less than the tvg_animatio_total_frame(). @@ -2257,13 +2285,18 @@ TVG_API Tvg_Animation* tvg_animation_new(void); * \retval TVG_RESULT_INSUFFICIENT_CONDITION No animatable data loaded from the Picture. * \retval TVG_RESULT_NOT_SUPPORTED The picture data does not support animations. * +* \note For efficiency, ThorVG ignores updates to the new frame value if the difference from the current frame value +* is less than 0.001. In such cases, it returns @c Result::InsufficientCondition. +* Values less than 0.001 may be disregarded and may not be accurately retained by the Animation. * \see tvg_animation_get_total_frame() +* +* \since 0.13 */ TVG_API Tvg_Result tvg_animation_set_frame(Tvg_Animation* animation, float no); /*! -* \brief Retrieves a picture instance associated with this animation instance. (Experimental API) +* \brief Retrieves a picture instance associated with this animation instance. * * This function provides access to the picture instance that can be used to load animation formats, such as Lottie(json). * After setting up the picture, it can be pushed to the designated canvas, enabling control over animation frames @@ -2274,12 +2307,14 @@ TVG_API Tvg_Result tvg_animation_set_frame(Tvg_Animation* animation, float no); * \return A picture instance that is tied to this animation. * * \warning The picture instance is owned by Animation. It should not be deleted manually. +* +* \since 0.13 */ TVG_API Tvg_Paint* tvg_animation_get_picture(Tvg_Animation* animation); /*! -* \brief Retrieves the current frame number of the animation. (Experimental API) +* \brief Retrieves the current frame number of the animation. * * \param[in] animation A Tvg_Animation pointer to the animation object. * \param[in] no The current frame number of the animation, between 0 and totalFrame() - 1. @@ -2290,12 +2325,14 @@ TVG_API Tvg_Paint* tvg_animation_get_picture(Tvg_Animation* animation); * * \see tvg_animation_get_total_frame() * \see tvg_animation_set_frame() +* +* \since 0.13 */ TVG_API Tvg_Result tvg_animation_get_frame(Tvg_Animation* animation, float* no); /*! -* \brief Retrieves the total number of frames in the animation. (Experimental API) +* \brief Retrieves the total number of frames in the animation. * * \param[in] animation A Tvg_Animation pointer to the animation object. * \param[in] cnt The total number of frames in the animation. @@ -2306,12 +2343,14 @@ TVG_API Tvg_Result tvg_animation_get_frame(Tvg_Animation* animation, float* no); * * \note Frame numbering starts from 0. * \note If the Picture is not properly configured, this function will return 0. +* +* \since 0.13 */ TVG_API Tvg_Result tvg_animation_get_total_frame(Tvg_Animation* animation, float* cnt); /*! -* \brief Retrieves the duration of the animation in seconds. (Experimental API) +* \brief Retrieves the duration of the animation in seconds. * * \param[in] animation A Tvg_Animation pointer to the animation object. * \param[in] duration The duration of the animation in seconds. @@ -2321,10 +2360,44 @@ TVG_API Tvg_Result tvg_animation_get_total_frame(Tvg_Animation* animation, float * \retval TVG_RESULT_INVALID_ARGUMENT An invalid Tvg_Animation pointer or @p duration. * * \note If the Picture is not properly configured, this function will return 0. +* +* \since 0.13 */ TVG_API Tvg_Result tvg_animation_get_duration(Tvg_Animation* animation, float* duration); +/*! +* \brief Specifies the playback segment of the animation. (Experimental API) +* +* \param[in] animation The Tvg_Animation pointer to the animation object. +* \param[in] begin segment begin. +* \param[in] end segment end. +* +* \return Tvg_Result enumeration. +* \retval TVG_RESULT_SUCCESS Succeed. +* \retval TVG_RESULT_INSUFFICIENT_CONDITION In case the animation is not loaded. +* \retval TVG_RESULT_INVALID_ARGUMENT When the given parameters are out of range. +* +* \since 0.13 +*/ +TVG_API Tvg_Result tvg_animation_set_segment(Tvg_Animation* animation, float begin, float end); + + +/*! +* \brief Gets the current segment. (Experimental API) +* +* \param[in] animation The Tvg_Animation pointer to the animation object. +* \param[out] begin segment begin. +* \param[out] end segment end. +* +* \return Tvg_Result enumeration. +* \retval TVG_RESULT_SUCCESS Succeed. +* \retval TVG_RESULT_INSUFFICIENT_CONDITION In case the animation is not loaded. +* \retval TVG_RESULT_INVALID_ARGUMENT When the given parameters are @c nullptr. +*/ +TVG_API Tvg_Result tvg_animation_get_segment(Tvg_Animation* animation, float* begin, float* end); + + /*! * \brief Deletes the given Tvg_Animation object. * @@ -2333,6 +2406,8 @@ TVG_API Tvg_Result tvg_animation_get_duration(Tvg_Animation* animation, float* d * \return Tvg_Result enumeration. * \retval TVG_RESULT_SUCCESS Succeed. * \retval TVG_RESULT_INVALID_ARGUMENT An invalid Tvg_Animation pointer. +* +* \since 0.13 */ TVG_API Tvg_Result tvg_animation_del(Tvg_Animation* animation); @@ -2364,9 +2439,9 @@ TVG_API Tvg_Animation* tvg_lottie_animation_new(void); * \brief Override the lottie properties through the slot data. (Experimental API) * * \param[in] animation The Tvg_Animation object to override the property with the slot. -* \param[in] slot The lottie slot data in json. +* \param[in] slot The Lottie slot data in json, or @c nullptr to reset. * -* \return Tvg_Animation A new Tvg_LottieAnimation object. +* \return Tvg_Result enumeration. * \retval TVG_RESULT_SUCCESS Succeed. * \retval TVG_RESULT_INSUFFICIENT_CONDITION In case the animation is not loaded. * \retval TVG_RESULT_INVALID_ARGUMENT When the given @p slot is invalid @@ -2375,6 +2450,48 @@ TVG_API Tvg_Animation* tvg_lottie_animation_new(void); TVG_API Tvg_Result tvg_lottie_animation_override(Tvg_Animation* animation, const char* slot); +/*! +* \brief Specifies a segment by marker. (Experimental API) +* +* \param[in] animation The Tvg_Animation pointer to the Lottie animation object. +* \param[in] marker The name of the segment marker. +* +* \return Tvg_Result enumeration. +* \retval TVG_RESULT_SUCCESS Succeed. +* \retval TVG_RESULT_INSUFFICIENT_CONDITION In case the animation is not loaded. +* \retval TVG_RESULT_INVALID_ARGUMENT When the given @p marker is invalid. +* \retval TVG_RESULT_NOT_SUPPORTED The Lottie Animation is not supported. +*/ +TVG_API Tvg_Result tvg_lottie_animation_set_marker(Tvg_Animation* animation, const char* marker); + + +/*! +* \brief Gets the marker count of the animation. (Experimental API) +* +* \param[in] animation The Tvg_Animation pointer to the Lottie animation object. +* \param[out] cnt The count value of the merkers. +* +* \return Tvg_Result enumeration. +* \retval TVG_RESULT_SUCCESS Succeed. +* \retval TVG_RESULT_INVALID_ARGUMENT In case a @c nullptr is passed as the argument. +*/ +TVG_API Tvg_Result tvg_lottie_animation_get_markers_cnt(Tvg_Animation* animation, uint32_t* cnt); + + +/*! +* \brief Gets the marker name by a given index. (Experimental API) +* +* \param[in] animation The Tvg_Animation pointer to the Lottie animation object. +* \param[in] idx The index of the animation marker, starts from 0. +* \param[out] name The name of marker when succeed. +* +* \return Tvg_Result enumeration. +* \retval TVG_RESULT_SUCCESS Succeed. +* \retval TVG_RESULT_INVALID_ARGUMENT In case @c nullptr is passed as the argument or @c idx is out of range. +*/ +TVG_API Tvg_Result tvg_lottie_animation_get_marker(Tvg_Animation* animation, uint32_t idx, const char** name); + + /** \} */ // end addtogroup ThorVGCapi_LottieAnimation diff --git a/src/libs/thorvg/thorvg_lottie.h b/src/libs/thorvg/thorvg_lottie.h index 6b5d56cdd..b69410ae9 100644 --- a/src/libs/thorvg/thorvg_lottie.h +++ b/src/libs/thorvg/thorvg_lottie.h @@ -14,7 +14,7 @@ namespace tvg * This class extends the Animation and has additional interfaces. * * @see Animation - * + * * @note Experimental API */ @@ -28,7 +28,7 @@ public: /** * @brief Override Lottie properties using slot data. * - * @param[in] slot The Lottie slot data in JSON format. + * @param[in] slot The Lottie slot data in JSON format to override, or @c nullptr to reset. * * @retval Result::Success When succeed. * @retval Result::InsufficientCondition In case the animation is not loaded. @@ -38,6 +38,50 @@ public: */ Result override(const char* slot) noexcept; + /** + * @brief Specifies a segment by marker. + * + * Markers are used to control animation playback by specifying start and end points, + * eliminating the need to know the exact frame numbers. + * Generally, markers are designated at the design level, + * meaning the callers must know the marker name in advance to use it. + * + * @param[in] marker The name of the segment marker. + * + * @retval Result::Success When successful. + * @retval Result::InsufficientCondition If the animation is not loaded. + * @retval Result::InvalidArguments When the given parameter is invalid. + * @retval Result::NonSupport When it's not animatable. + * + * @note If a @c marker is specified, the previously set segment will be disregarded. + * @note Set @c nullptr to reset the specified segment. + * @see Animation::segment(float begin, float end) + * @note Experimental API + */ + Result segment(const char* marker) noexcept; + + /** + * @brief Gets the marker count of the animation. + * + * @retval The count of the markers, zero if there is no marker. + * + * @see LottieAnimation::marker() + * @note Experimental API + */ + uint32_t markersCnt() noexcept; + + /** + * @brief Gets the marker name by a given index. + * + * @param[in] idx The index of the animation marker, starts from 0. + * + * @retval The name of marker when succeed, @c nullptr otherwise. + * + * @see LottieAnimation::markersCnt() + * @note Experimental API + */ + const char* marker(uint32_t idx) noexcept; + /** * @brief Creates a new LottieAnimation object. * diff --git a/src/libs/thorvg/tvgAnimation.cpp b/src/libs/thorvg/tvgAnimation.cpp index 82c08e8a4..399ee4589 100644 --- a/src/libs/thorvg/tvgAnimation.cpp +++ b/src/libs/thorvg/tvgAnimation.cpp @@ -96,6 +96,33 @@ float Animation::duration() const noexcept } +Result Animation::segment(float begin, float end) noexcept +{ + if (begin < 0.0 || end > 1.0 || begin >= end) return Result::InvalidArguments; + + auto loader = pImpl->picture->pImpl->loader; + if (!loader) return Result::InsufficientCondition; + if (!loader->animatable()) return Result::NonSupport; + + static_cast(loader)->segment(begin, end); + + return Result::Success; +} + + +Result Animation::segment(float *begin, float *end) noexcept +{ + auto loader = pImpl->picture->pImpl->loader; + if (!loader) return Result::InsufficientCondition; + if (!loader->animatable()) return Result::NonSupport; + if (!begin && !end) return Result::InvalidArguments; + + static_cast(loader)->segment(begin, end); + + return Result::Success; +} + + unique_ptr Animation::gen() noexcept { return unique_ptr(new Animation); diff --git a/src/libs/thorvg/tvgArray.h b/src/libs/thorvg/tvgArray.h index 7ab8f13fd..33cd55ba2 100644 --- a/src/libs/thorvg/tvgArray.h +++ b/src/libs/thorvg/tvgArray.h @@ -64,6 +64,7 @@ struct Array void push(Array& rhs) { + if (rhs.count == 0) return; grow(rhs.count); memcpy(data + count, rhs.data, rhs.count * sizeof(T)); count += rhs.count; diff --git a/src/libs/thorvg/tvgCanvas.cpp b/src/libs/thorvg/tvgCanvas.cpp index 6b81c4d57..22a97c20d 100644 --- a/src/libs/thorvg/tvgCanvas.cpp +++ b/src/libs/thorvg/tvgCanvas.cpp @@ -84,6 +84,12 @@ Result Canvas::update(Paint* paint) noexcept } +Result Canvas::viewport(int32_t x, int32_t y, int32_t w, int32_t h) noexcept +{ + return pImpl->viewport(x, y, w, h); +} + + Result Canvas::sync() noexcept { return pImpl->sync(); diff --git a/src/libs/thorvg/tvgCanvas.h b/src/libs/thorvg/tvgCanvas.h index 12bb84b1a..6fe923a2e 100644 --- a/src/libs/thorvg/tvgCanvas.h +++ b/src/libs/thorvg/tvgCanvas.h @@ -31,10 +31,14 @@ struct Canvas::Impl { + enum Status : uint8_t {Synced = 0, Updating, Drawing}; + list paints; RenderMethod* renderer; + RenderRegion vport = {0, 0, INT32_MAX, INT32_MAX}; + Status status = Status::Synced; + bool refresh = false; //if all paints should be updated by force. - bool drawing = false; //on drawing condition? Impl(RenderMethod* pRenderer) : renderer(pRenderer) { @@ -44,14 +48,12 @@ struct Canvas::Impl ~Impl() { //make it sure any deffered jobs - if (renderer) { - renderer->sync(); - renderer->clear(); - } + renderer->sync(); + renderer->clear(); clearPaints(); - if (renderer && (renderer->unref() == 0)) delete(renderer); + if (renderer->unref() == 0) delete(renderer); } void clearPaints() @@ -65,7 +67,7 @@ struct Canvas::Impl Result push(unique_ptr paint) { //You can not push paints during rendering. - if (drawing) return Result::InsufficientCondition; + if (status == Status::Drawing) return Result::InsufficientCondition; auto p = paint.release(); if (!p) return Result::MemoryCorruption; @@ -78,12 +80,12 @@ struct Canvas::Impl Result clear(bool free) { //Clear render target before drawing - if (!renderer || !renderer->clear()) return Result::InsufficientCondition; + if (!renderer->clear()) return Result::InsufficientCondition; //Free paints if (free) clearPaints(); - drawing = false; + status = Status::Synced; return Result::Success; } @@ -95,37 +97,27 @@ struct Canvas::Impl Result update(Paint* paint, bool force) { - if (paints.empty() || drawing || !renderer) return Result::InsufficientCondition; + if (paints.empty() || status == Status::Drawing) return Result::InsufficientCondition; Array clips; auto flag = RenderUpdateFlag::None; if (refresh || force) flag = RenderUpdateFlag::All; - //Update single paint node if (paint) { - //Optimize Me: Can we skip the searching? - for (auto paint2 : paints) { - if (paint2 == paint) { - paint->pImpl->update(renderer, nullptr, clips, 255, flag); - return Result::Success; - } - } - return Result::InvalidArguments; - //Update all retained paint nodes + paint->pImpl->update(renderer, nullptr, clips, 255, flag); } else { for (auto paint : paints) { paint->pImpl->update(renderer, nullptr, clips, 255, flag); } + refresh = false; } - - refresh = false; - + status = Status::Updating; return Result::Success; } Result draw() { - if (drawing || paints.empty() || !renderer || !renderer->preRender()) return Result::InsufficientCondition; + if (status == Status::Drawing || paints.empty() || !renderer->preRender()) return Result::InsufficientCondition; bool rendered = false; for (auto paint : paints) { @@ -134,22 +126,37 @@ struct Canvas::Impl if (!rendered || !renderer->postRender()) return Result::InsufficientCondition; - drawing = true; - + status = Status::Drawing; return Result::Success; } Result sync() { - if (!drawing) return Result::InsufficientCondition; + if (status == Status::Synced) return Result::InsufficientCondition; if (renderer->sync()) { - drawing = false; + status = Status::Synced; return Result::Success; } return Result::InsufficientCondition; } + + Result viewport(int32_t x, int32_t y, int32_t w, int32_t h) + { + if (status != Status::Synced) return Result::InsufficientCondition; + RenderRegion val = {x, y, w, h}; + //intersect if the target buffer is already set. + auto surface = renderer->mainSurface(); + if (surface && surface->w > 0 && surface->h > 0) { + val.intersect({0, 0, (int32_t)surface->w, (int32_t)surface->h}); + } + if (vport == val) return Result::Success; + renderer->viewport(val); + vport = val; + needRefresh(); + return Result::Success; + } }; #endif /* _TVG_CANVAS_H_ */ diff --git a/src/libs/thorvg/tvgCapi.cpp b/src/libs/thorvg/tvgCapi.cpp index de4e61095..a6c2b360e 100644 --- a/src/libs/thorvg/tvgCapi.cpp +++ b/src/libs/thorvg/tvgCapi.cpp @@ -26,8 +26,10 @@ #include "config.h" #include #include "thorvg.h" -#include "thorvg_lottie.h" #include "thorvg_capi.h" +#ifdef THORVG_LOTTIE_LOADER_SUPPORT +#include "thorvg_lottie.h" +#endif using namespace std; using namespace tvg; @@ -112,6 +114,13 @@ TVG_API Tvg_Result tvg_canvas_update(Tvg_Canvas* canvas) } +TVG_API Tvg_Result tvg_canvas_est_viewport(Tvg_Canvas* canvas, int32_t x, int32_t y, int32_t w, int32_t h) +{ + if (!canvas) return TVG_RESULT_INVALID_ARGUMENT; + return (Tvg_Result) reinterpret_cast(canvas)->viewport(x, y, w, h); +} + + TVG_API Tvg_Result tvg_canvas_update_paint(Tvg_Canvas* canvas, Tvg_Paint* paint) { if (!canvas || !paint) return TVG_RESULT_INVALID_ARGUMENT; @@ -771,6 +780,20 @@ TVG_API Tvg_Result tvg_animation_get_duration(Tvg_Animation* animation, float* d } +TVG_API Tvg_Result tvg_animation_set_segment(Tvg_Animation* animation, float start, float end) +{ + if (!animation) return TVG_RESULT_INVALID_ARGUMENT; + return (Tvg_Result) reinterpret_cast(animation)->segment(start, end); +} + + +TVG_API Tvg_Result tvg_animation_get_segment(Tvg_Animation* animation, float* start, float* end) +{ + if (!animation) return TVG_RESULT_INVALID_ARGUMENT; + return (Tvg_Result) reinterpret_cast(animation)->segment(start, end); +} + + TVG_API Tvg_Result tvg_animation_del(Tvg_Animation* animation) { if (!animation) return TVG_RESULT_INVALID_ARGUMENT; @@ -801,6 +824,39 @@ TVG_API Tvg_Result tvg_lottie_animation_override(Tvg_Animation* animation, const return TVG_RESULT_NOT_SUPPORTED; } + +TVG_API Tvg_Result tvg_lottie_animation_set_marker(Tvg_Animation* animation, const char* marker) +{ +#ifdef THORVG_LOTTIE_LOADER_SUPPORT + if (!animation) return TVG_RESULT_INVALID_ARGUMENT; + return (Tvg_Result) reinterpret_cast(animation)->segment(marker); +#endif + return TVG_RESULT_NOT_SUPPORTED; +} + + +TVG_API Tvg_Result tvg_lottie_animation_get_markers_cnt(Tvg_Animation* animation, uint32_t* cnt) +{ +#ifdef THORVG_LOTTIE_LOADER_SUPPORT + if (!animation || !cnt) return TVG_RESULT_INVALID_ARGUMENT; + *cnt = reinterpret_cast(animation)->markersCnt(); + return TVG_RESULT_SUCCESS; +#endif + return TVG_RESULT_NOT_SUPPORTED; +} + + +TVG_API Tvg_Result tvg_lottie_animation_get_marker(Tvg_Animation* animation, uint32_t idx, const char** name) +{ +#ifdef THORVG_LOTTIE_LOADER_SUPPORT + if (!animation || !name) return TVG_RESULT_INVALID_ARGUMENT; + *name = reinterpret_cast(animation)->marker(idx); + if (!(*name)) return TVG_RESULT_INVALID_ARGUMENT; + return TVG_RESULT_SUCCESS; +#endif + return TVG_RESULT_NOT_SUPPORTED; +} + #ifdef __cplusplus } #endif diff --git a/src/libs/thorvg/tvgCompressor.cpp b/src/libs/thorvg/tvgCompressor.cpp index 940ff3e81..d0c7c4ecb 100644 --- a/src/libs/thorvg/tvgCompressor.cpp +++ b/src/libs/thorvg/tvgCompressor.cpp @@ -461,11 +461,11 @@ size_t b64Decode(const char* encoded, const size_t len, char** decoded) auto value2 = B64_INDEX[(size_t)encoded[1]]; output[idx++] = (value1 << 2) + ((value2 & 0x30) >> 4); - if (!encoded[2] || encoded[2] == '=' || encoded[2] == '.') break; + if (!encoded[2] || encoded[3] < 0 || encoded[2] == '=' || encoded[2] == '.') break; auto value3 = B64_INDEX[(size_t)encoded[2]]; output[idx++] = ((value2 & 0x0f) << 4) + ((value3 & 0x3c) >> 2); - if (!encoded[3] || encoded[3] == '=' || encoded[3] == '.') break; + if (!encoded[3] || encoded[3] < 0 || encoded[3] == '=' || encoded[3] == '.') break; auto value4 = B64_INDEX[(size_t)encoded[3]]; output[idx++] = ((value3 & 0x03) << 6) + value4; encoded += 4; diff --git a/src/libs/thorvg/tvgFrameModule.h b/src/libs/thorvg/tvgFrameModule.h index dca4a7c18..02e69a87a 100644 --- a/src/libs/thorvg/tvgFrameModule.h +++ b/src/libs/thorvg/tvgFrameModule.h @@ -34,6 +34,9 @@ namespace tvg class FrameModule: public ImageLoader { public: + float segmentBegin = 0.0f; + float segmentEnd = 1.0f; + FrameModule(FileType type) : ImageLoader(type) {} virtual ~FrameModule() {} @@ -42,6 +45,18 @@ public: virtual float curFrame() = 0; //return the current frame number virtual float duration() = 0; //return the animation duration in seconds + void segment(float* begin, float* end) + { + if (begin) *begin = segmentBegin; + if (end) *end = segmentEnd; + } + + void segment(float begin, float end) + { + segmentBegin = begin; + segmentEnd = end; + } + virtual bool animatable() override { return true; } }; diff --git a/src/libs/thorvg/tvgGlCanvas.cpp b/src/libs/thorvg/tvgGlCanvas.cpp index 806fb936a..d246ea551 100644 --- a/src/libs/thorvg/tvgGlCanvas.cpp +++ b/src/libs/thorvg/tvgGlCanvas.cpp @@ -63,14 +63,16 @@ GlCanvas::~GlCanvas() } -Result GlCanvas::target(uint32_t* buffer, uint32_t stride, uint32_t w, uint32_t h) noexcept +Result GlCanvas::target(int32_t id, uint32_t w, uint32_t h) noexcept { #ifdef THORVG_GL_RASTER_SUPPORT //We know renderer type, avoid dynamic_cast for performance. auto renderer = static_cast(Canvas::pImpl->renderer); if (!renderer) return Result::MemoryCorruption; - if (!renderer->target(buffer, stride, w, h)) return Result::Unknown; + if (!renderer->target(id, w, h)) return Result::Unknown; + Canvas::pImpl->vport = {0, 0, (int32_t)w, (int32_t)h}; + renderer->viewport(Canvas::pImpl->vport); //Paints must be updated again with this new target. Canvas::pImpl->needRefresh(); diff --git a/src/libs/thorvg/tvgBezier.cpp b/src/libs/thorvg/tvgLines.cpp similarity index 92% rename from src/libs/thorvg/tvgBezier.cpp rename to src/libs/thorvg/tvgLines.cpp index ab623ba38..3bdab769e 100644 --- a/src/libs/thorvg/tvgBezier.cpp +++ b/src/libs/thorvg/tvgLines.cpp @@ -24,9 +24,9 @@ #if LV_USE_THORVG_INTERNAL #include "tvgMath.h" -#include "tvgBezier.h" +#include "tvgLines.h" -#define BEZIER_EPSILON 1e-4f +#define BEZIER_EPSILON 1e-2f /************************************************************************/ /* Internal Class Implementation */ @@ -104,6 +104,25 @@ float _bezAt(const Bezier& bz, float at, float length, LengthFunc lineLengthFunc namespace tvg { +float lineLength(const Point& pt1, const Point& pt2) +{ + return _lineLength(pt1, pt2); +} + + +void lineSplitAt(const Line& cur, float at, Line& left, Line& right) +{ + auto len = lineLength(cur.pt1, cur.pt2); + auto dx = ((cur.pt2.x - cur.pt1.x) / len) * at; + auto dy = ((cur.pt2.y - cur.pt1.y) / len) * at; + left.pt1 = cur.pt1; + left.pt2.x = left.pt1.x + dx; + left.pt2.y = left.pt1.y + dy; + right.pt1 = left.pt2; + right.pt2 = cur.pt2; +} + + void bezSplit(const Bezier& cur, Bezier& left, Bezier& right) { auto c = (cur.ctrl1.x + cur.ctrl2.x) * 0.5f; @@ -222,11 +241,9 @@ float bezAngleAt(const Bezier& bz, float t) pt.x *= 3; pt.y *= 3; - return atan2(pt.x, pt.y) * 180.0f / 3.141592f; + return mathRad2Deg(atan2(pt.x, pt.y)); } - } #endif /* LV_USE_THORVG_INTERNAL */ - diff --git a/src/libs/thorvg/tvgBezier.h b/src/libs/thorvg/tvgLines.h similarity index 88% rename from src/libs/thorvg/tvgBezier.h rename to src/libs/thorvg/tvgLines.h index 96579b4a0..804ced13a 100644 --- a/src/libs/thorvg/tvgBezier.h +++ b/src/libs/thorvg/tvgLines.h @@ -23,14 +23,24 @@ #include "../../lv_conf_internal.h" #if LV_USE_THORVG_INTERNAL -#ifndef _TVG_BEZIER_H_ -#define _TVG_BEZIER_H_ +#ifndef _TVG_LINES_H_ +#define _TVG_LINES_H_ #include "tvgCommon.h" namespace tvg { +struct Line +{ + Point pt1; + Point pt2; +}; + +float lineLength(const Point& pt1, const Point& pt2); +void lineSplitAt(const Line& cur, float at, Line& left, Line& right); + + struct Bezier { Point start; @@ -51,7 +61,6 @@ float bezLengthApprox(const Bezier& cur); float bezAtApprox(const Bezier& bz, float at, float length); } -#endif //_TVG_BEZIER_H_ +#endif //_TVG_LINES_H_ #endif /* LV_USE_THORVG_INTERNAL */ - diff --git a/src/libs/thorvg/tvgLoader.cpp b/src/libs/thorvg/tvgLoader.cpp index 7d64a6993..eb4ba187f 100644 --- a/src/libs/thorvg/tvgLoader.cpp +++ b/src/libs/thorvg/tvgLoader.cpp @@ -181,7 +181,6 @@ static LoadModule* _findByPath(const string& path) if (!ext.compare("tvg")) return _find(FileType::Tvg); if (!ext.compare("svg")) return _find(FileType::Svg); if (!ext.compare("json")) return _find(FileType::Lottie); - if (!ext.compare("lottie")) return _find(FileType::Lottie); if (!ext.compare("png")) return _find(FileType::Png); if (!ext.compare("jpg")) return _find(FileType::Jpg); if (!ext.compare("webp")) return _find(FileType::Webp); @@ -274,7 +273,7 @@ bool LoaderMgr::term() auto tmp = loader; loader = loader->next; _activeLoaders.remove(tmp); - if (ret) delete(loader); + if (ret) delete(tmp); } return true; } @@ -298,15 +297,24 @@ LoadModule* LoaderMgr::loader(const string& path, bool* invalid) { *invalid = false; - if (auto loader = _findFromCache(path)) return loader; + //TODO: lottie is not sharable. + auto allowCache = true; + auto ext = path.substr(path.find_last_of(".") + 1); + if (!ext.compare("json")) allowCache = false; + + if (allowCache) { + if (auto loader = _findFromCache(path)) return loader; + } if (auto loader = _findByPath(path)) { if (loader->open(path)) { - loader->hashpath = strdup(path.c_str()); - loader->pathcache = true; - { - ScopedLock lock(key); - _activeLoaders.back(loader); + if (allowCache) { + loader->hashpath = strdup(path.c_str()); + loader->pathcache = true; + { + ScopedLock lock(key); + _activeLoaders.back(loader); + } } return loader; } @@ -316,11 +324,13 @@ LoadModule* LoaderMgr::loader(const string& path, bool* invalid) for (int i = 0; i < static_cast(FileType::Raw); i++) { if (auto loader = _find(static_cast(i))) { if (loader->open(path)) { - loader->hashpath = strdup(path.c_str()); - loader->pathcache = true; - { - ScopedLock lock(key); - _activeLoaders.back(loader); + if (allowCache) { + loader->hashpath = strdup(path.c_str()); + loader->pathcache = true; + { + ScopedLock lock(key); + _activeLoaders.back(loader); + } } return loader; } @@ -357,7 +367,15 @@ LoadModule* LoaderMgr::loader(const char* data, uint32_t size, const string& mim { //Note that users could use the same data pointer with the different content. //Thus caching is only valid for shareable. - if (!copy) { + auto allowCache = !copy; + + //TODO: lottie is not sharable. + if (allowCache) { + auto type = _convert(mimeType); + if (type == FileType::Lottie) allowCache = false; + } + + if (allowCache) { if (auto loader = _findFromCache(data, size, mimeType)) return loader; } @@ -365,7 +383,7 @@ LoadModule* LoaderMgr::loader(const char* data, uint32_t size, const string& mim if (!mimeType.empty()) { if (auto loader = _findByType(mimeType)) { if (loader->open(data, size, copy)) { - if (!copy) { + if (allowCache) { loader->hashkey = HASH_KEY(data); ScopedLock lock(key); _activeLoaders.back(loader); @@ -382,7 +400,7 @@ LoadModule* LoaderMgr::loader(const char* data, uint32_t size, const string& mim auto loader = _find(static_cast(i)); if (loader) { if (loader->open(data, size, copy)) { - if (!copy) { + if (allowCache) { loader->hashkey = HASH_KEY(data); ScopedLock lock(key); _activeLoaders.back(loader); diff --git a/src/libs/thorvg/tvgLottieAnimation.cpp b/src/libs/thorvg/tvgLottieAnimation.cpp index f6e0b62bb..69818b37d 100644 --- a/src/libs/thorvg/tvgLottieAnimation.cpp +++ b/src/libs/thorvg/tvgLottieAnimation.cpp @@ -41,6 +41,7 @@ LottieAnimation::~LottieAnimation() { } + Result LottieAnimation::override(const char* slot) noexcept { if (!pImpl->picture->pImpl->loader) return Result::InsufficientCondition; @@ -53,6 +54,41 @@ Result LottieAnimation::override(const char* slot) noexcept } +Result LottieAnimation::segment(const char* marker) noexcept +{ + auto loader = pImpl->picture->pImpl->loader; + if (!loader) return Result::InsufficientCondition; + if (!loader->animatable()) return Result::NonSupport; + + if (!marker) { + static_cast(loader)->segment(0.0f, 1.0f); + return Result::Success; + } + + float begin, end; + if (!static_cast(loader)->segment(marker, begin, end)) { + return Result::InvalidArguments; + } + return static_cast(this)->segment(begin, end); +} + + +uint32_t LottieAnimation::markersCnt() noexcept +{ + auto loader = pImpl->picture->pImpl->loader; + if (!loader || !loader->animatable()) return 0; + return static_cast(loader)->markersCnt(); +} + + +const char* LottieAnimation::marker(uint32_t idx) noexcept +{ + auto loader = pImpl->picture->pImpl->loader; + if (!loader || !loader->animatable()) return nullptr; + return static_cast(loader)->markers(idx); +} + + unique_ptr LottieAnimation::gen() noexcept { return unique_ptr(new LottieAnimation); diff --git a/src/libs/thorvg/tvgLottieBuilder.cpp b/src/libs/thorvg/tvgLottieBuilder.cpp index 3fd42c3d9..707c7993f 100644 --- a/src/libs/thorvg/tvgLottieBuilder.cpp +++ b/src/libs/thorvg/tvgLottieBuilder.cpp @@ -30,9 +30,10 @@ #include "tvgPaint.h" #include "tvgShape.h" #include "tvgInlist.h" +#include "tvgTaskScheduler.h" #include "tvgLottieModel.h" #include "tvgLottieBuilder.h" -#include "tvgTaskScheduler.h" +#include "tvgLottieExpressions.h" /************************************************************************/ @@ -62,10 +63,11 @@ struct RenderContext Shape* merging = nullptr; //merging shapes if possible (if shapes have same properties) LottieObject** begin = nullptr; //iteration entry point RenderRepeater* repeater = nullptr; + Matrix* transform = nullptr; float roundness = 0.0f; bool fragmenting = false; //render context has been fragmented by filling bool reqFragment = false; //requirment to fragment the render context - bool allowMerging = true; //individual trimpath doesn't allow merging shapes + bool ownPropagator = true; //this rendering context shares the propergator RenderContext() { @@ -74,13 +76,21 @@ struct RenderContext ~RenderContext() { - delete(propagator); + if (ownPropagator) delete(propagator); delete(repeater); + free(transform); } - RenderContext(const RenderContext& rhs) + RenderContext(const RenderContext& rhs, bool mergeable = false) { - propagator = static_cast(rhs.propagator->duplicate()); + if (mergeable) { + this->ownPropagator = false; + propagator = rhs.propagator; + merging = rhs.merging; + } else { + propagator = static_cast(rhs.propagator->duplicate()); + } + if (rhs.repeater) { repeater = new RenderRepeater(); *repeater = *rhs.repeater; @@ -90,14 +100,15 @@ struct RenderContext }; -static void _updateChildren(LottieGroup* parent, float frameNo, Inlist& contexts); -static void _updateLayer(LottieLayer* root, LottieLayer* layer, float frameNo); +static void _updateChildren(LottieGroup* parent, float frameNo, Inlist& contexts, LottieExpressions* exps); +static void _updateLayer(LottieLayer* root, LottieLayer* layer, float frameNo, LottieExpressions* exps); static bool _buildComposition(LottieComposition* comp, LottieGroup* parent); +static Shape* _draw(LottieGroup* parent, RenderContext* ctx); static void _rotateX(Matrix* m, float degree) { if (degree == 0.0f) return; - auto radian = degree / 180.0f * M_PI; + auto radian = mathDeg2Rad(degree); m->e22 *= cosf(radian); } @@ -105,7 +116,7 @@ static void _rotateX(Matrix* m, float degree) static void _rotateY(Matrix* m, float degree) { if (degree == 0.0f) return; - auto radian = degree / 180.0f * M_PI; + auto radian = mathDeg2Rad(degree); m->e11 *= cosf(radian); } @@ -113,7 +124,7 @@ static void _rotateY(Matrix* m, float degree) static void _rotationZ(Matrix* m, float degree) { if (degree == 0.0f) return; - auto radian = degree / 180.0f * M_PI; + auto radian = mathDeg2Rad(degree); m->e11 = cosf(radian); m->e12 = -sinf(radian); m->e21 = sinf(radian); @@ -121,7 +132,43 @@ static void _rotationZ(Matrix* m, float degree) } -static bool _updateTransform(LottieTransform* transform, float frameNo, bool autoOrient, Matrix& matrix, uint8_t& opacity) +static void _skew(Matrix* m, float angleDeg, float axisDeg) +{ + auto angle = -mathDeg2Rad(angleDeg); + float tanVal = tanf(angle); + + axisDeg = fmod(axisDeg, 180.0f); + if (fabsf(axisDeg) < 0.01f || fabsf(axisDeg - 180.0f) < 0.01f || fabsf(axisDeg + 180.0f) < 0.01f) { + float cosVal = cosf(mathDeg2Rad(axisDeg)); + auto B = cosVal * cosVal * tanVal; + m->e12 += B * m->e11; + m->e22 += B * m->e21; + return; + } else if (fabsf(axisDeg - 90.0f) < 0.01f || fabsf(axisDeg + 90.0f) < 0.01f) { + float sinVal = -sinf(mathDeg2Rad(axisDeg)); + auto C = sinVal * sinVal * tanVal; + m->e11 -= C * m->e12; + m->e21 -= C * m->e22; + return; + } + + auto axis = -mathDeg2Rad(axisDeg); + float cosVal = cosf(axis); + float sinVal = sinf(axis); + auto A = sinVal * cosVal * tanVal; + auto B = cosVal * cosVal * tanVal; + auto C = sinVal * sinVal * tanVal; + + auto e11 = m->e11; + auto e21 = m->e21; + m->e11 = (1.0f - A) * e11 - C * m->e12; + m->e12 = B * e11 + (1.0f + A) * m->e12; + m->e21 = (1.0f - A) * e21 - C * m->e22; + m->e22 = B * e21 + (1.0f + A) * m->e22; +} + + +static bool _updateTransform(LottieTransform* transform, float frameNo, bool autoOrient, Matrix& matrix, uint8_t& opacity, LottieExpressions* exps) { mathIdentity(&matrix); @@ -133,46 +180,55 @@ static bool _updateTransform(LottieTransform* transform, float frameNo, bool aut if (transform->coords) { mathTranslate(&matrix, transform->coords->x(frameNo), transform->coords->y(frameNo)); } else { - auto position = transform->position(frameNo); + auto position = transform->position(frameNo, exps); mathTranslate(&matrix, position.x, position.y); } auto angle = 0.0f; if (autoOrient) angle = transform->position.angle(frameNo); - _rotationZ(&matrix, transform->rotation(frameNo) + angle); + _rotationZ(&matrix, transform->rotation(frameNo, exps) + angle); if (transform->rotationEx) { - _rotateY(&matrix, transform->rotationEx->y(frameNo)); - _rotateX(&matrix, transform->rotationEx->x(frameNo)); + _rotateY(&matrix, transform->rotationEx->y(frameNo, exps)); + _rotateX(&matrix, transform->rotationEx->x(frameNo, exps)); } - auto scale = transform->scale(frameNo); + auto skewAngle = transform->skewAngle(frameNo, exps); + if (skewAngle != 0.0f) { + // For angles where tangent explodes, the shape degenerates into an infinitely thin line. + // This is handled by zeroing out the matrix due to finite numerical precision. + skewAngle = fmod(skewAngle, 180.0f); + if (fabsf(skewAngle - 90.0f) < 0.01f || fabsf(skewAngle + 90.0f) < 0.01f) return false; + _skew(&matrix, skewAngle, transform->skewAxis(frameNo, exps)); + } + + auto scale = transform->scale(frameNo, exps); mathScaleR(&matrix, scale.x * 0.01f, scale.y * 0.01f); //Lottie specific anchor transform. - auto anchor = transform->anchor(frameNo); + auto anchor = transform->anchor(frameNo, exps); mathTranslateR(&matrix, -anchor.x, -anchor.y); //invisible just in case. if (scale.x == 0.0f || scale.y == 0.0f) opacity = 0; - else opacity = transform->opacity(frameNo); + else opacity = transform->opacity(frameNo, exps); return true; } -static void _updateTransform(LottieLayer* layer, float frameNo) +static void _updateTransform(LottieLayer* layer, float frameNo, LottieExpressions* exps) { if (!layer || mathEqual(layer->cache.frameNo, frameNo)) return; auto transform = layer->transform; auto parent = layer->parent; - if (parent) _updateTransform(parent, frameNo); + if (parent) _updateTransform(parent, frameNo, exps); auto& matrix = layer->cache.matrix; - _updateTransform(transform, frameNo, layer->autoOrient, matrix, layer->cache.opacity); + _updateTransform(transform, frameNo, layer->autoOrient, matrix, layer->cache.opacity, exps); if (parent) { if (!mathIdentity((const Matrix*) &parent->cache.matrix)) { @@ -184,16 +240,23 @@ static void _updateTransform(LottieLayer* layer, float frameNo) } -static void _updateTransform(TVG_UNUSED LottieGroup* parent, LottieObject** child, float frameNo, TVG_UNUSED Inlist& contexts, RenderContext* ctx) +static void _updateTransform(LottieGroup* parent, LottieObject** child, float frameNo, TVG_UNUSED Inlist& contexts, RenderContext* ctx, LottieExpressions* exps) { auto transform = static_cast(*child); if (!transform) return; + uint8_t opacity; + + if (parent->mergeable()) { + if (!ctx->transform) ctx->transform = (Matrix*)malloc(sizeof(Matrix)); + _updateTransform(transform, frameNo, false, *ctx->transform, opacity, exps); + return; + } + ctx->merging = nullptr; Matrix matrix; - uint8_t opacity; - if (!_updateTransform(transform, frameNo, false, matrix, opacity)) return; + if (!_updateTransform(transform, frameNo, false, matrix, opacity, exps)) return; auto pmatrix = PP(ctx->propagator)->transform(); ctx->propagator->transform(pmatrix ? mathMultiply(pmatrix, &matrix) : matrix); @@ -207,7 +270,7 @@ static void _updateTransform(TVG_UNUSED LottieGroup* parent, LottieObject** chil } -static void _updateGroup(LottieGroup* parent, LottieObject** child, float frameNo, TVG_UNUSED Inlist& pcontexts, RenderContext* ctx) +static void _updateGroup(LottieGroup* parent, LottieObject** child, float frameNo, TVG_UNUSED Inlist& pcontexts, RenderContext* ctx, LottieExpressions* exps) { auto group = static_cast(*child); @@ -217,27 +280,30 @@ static void _updateGroup(LottieGroup* parent, LottieObject** child, float frameN group->scene = parent->scene; group->reqFragment |= ctx->reqFragment; - Inlist contexts; - contexts.back(new RenderContext(*ctx)); + //generate a merging shape to consolidate partial shapes into a single entity + if (group->mergeable()) _draw(parent, ctx); - _updateChildren(group, frameNo, contexts); + Inlist contexts; + contexts.back(new RenderContext(*ctx, group->mergeable())); + + _updateChildren(group, frameNo, contexts, exps); contexts.free(); } -static void _updateStroke(LottieStroke* stroke, float frameNo, RenderContext* ctx) +static void _updateStroke(LottieStroke* stroke, float frameNo, RenderContext* ctx, LottieExpressions* exps) { - ctx->propagator->stroke(stroke->width(frameNo)); + ctx->propagator->stroke(stroke->width(frameNo, exps)); ctx->propagator->stroke(stroke->cap); ctx->propagator->stroke(stroke->join); ctx->propagator->strokeMiterlimit(stroke->miterLimit); if (stroke->dashattr) { float dashes[2]; - dashes[0] = stroke->dashSize(frameNo); - dashes[1] = dashes[0] + stroke->dashGap(frameNo); - P(ctx->propagator)->strokeDash(dashes, 2, stroke->dashOffset(frameNo)); + dashes[0] = stroke->dashSize(frameNo, exps); + dashes[1] = dashes[0] + stroke->dashGap(frameNo, exps); + P(ctx->propagator)->strokeDash(dashes, 2, stroke->dashOffset(frameNo, exps)); } else { ctx->propagator->stroke(nullptr, 0); } @@ -258,32 +324,32 @@ static bool _fragmented(LottieObject** child, Inlist& contexts, R } -static void _updateSolidStroke(TVG_UNUSED LottieGroup* parent, LottieObject** child, float frameNo, Inlist& contexts, RenderContext* ctx) +static void _updateSolidStroke(TVG_UNUSED LottieGroup* parent, LottieObject** child, float frameNo, Inlist& contexts, RenderContext* ctx, LottieExpressions* exps) { if (_fragmented(child, contexts, ctx)) return; auto stroke = static_cast(*child); ctx->merging = nullptr; - auto color = stroke->color(frameNo); - ctx->propagator->stroke(color.rgb[0], color.rgb[1], color.rgb[2], stroke->opacity(frameNo)); - _updateStroke(static_cast(stroke), frameNo, ctx); + auto color = stroke->color(frameNo, exps); + ctx->propagator->stroke(color.rgb[0], color.rgb[1], color.rgb[2], stroke->opacity(frameNo, exps)); + _updateStroke(static_cast(stroke), frameNo, ctx, exps); } -static void _updateGradientStroke(TVG_UNUSED LottieGroup* parent, LottieObject** child, float frameNo, Inlist& contexts, RenderContext* ctx) +static void _updateGradientStroke(TVG_UNUSED LottieGroup* parent, LottieObject** child, float frameNo, Inlist& contexts, RenderContext* ctx, LottieExpressions* exps) { if (_fragmented(child, contexts, ctx)) return; auto stroke = static_cast(*child); ctx->merging = nullptr; - ctx->propagator->stroke(unique_ptr(stroke->fill(frameNo))); - _updateStroke(static_cast(stroke), frameNo, ctx); + ctx->propagator->stroke(unique_ptr(stroke->fill(frameNo, exps))); + _updateStroke(static_cast(stroke), frameNo, ctx, exps); } -static void _updateSolidFill(TVG_UNUSED LottieGroup* parent, LottieObject** child, float frameNo, TVG_UNUSED Inlist& contexts, RenderContext* ctx) +static void _updateSolidFill(TVG_UNUSED LottieGroup* parent, LottieObject** child, float frameNo, TVG_UNUSED Inlist& contexts, RenderContext* ctx, LottieExpressions* exps) { if (_fragmented(child, contexts, ctx)) return; @@ -291,34 +357,32 @@ static void _updateSolidFill(TVG_UNUSED LottieGroup* parent, LottieObject** chil ctx->merging = nullptr; auto color = fill->color(frameNo); - ctx->propagator->fill(color.rgb[0], color.rgb[1], color.rgb[2], fill->opacity(frameNo)); + ctx->propagator->fill(color.rgb[0], color.rgb[1], color.rgb[2], fill->opacity(frameNo, exps)); ctx->propagator->fill(fill->rule); if (ctx->propagator->strokeWidth() > 0) ctx->propagator->order(true); } -static Shape* _updateGradientFill(TVG_UNUSED LottieGroup* parent, LottieObject** child, float frameNo, TVG_UNUSED Inlist& contexts, RenderContext* ctx) +static void _updateGradientFill(TVG_UNUSED LottieGroup* parent, LottieObject** child, float frameNo, TVG_UNUSED Inlist& contexts, RenderContext* ctx, LottieExpressions* exps) { - if (_fragmented(child, contexts, ctx)) return nullptr; + if (_fragmented(child, contexts, ctx)) return; auto fill = static_cast(*child); ctx->merging = nullptr; //TODO: reuse the fill instance? - ctx->propagator->fill(unique_ptr(fill->fill(frameNo))); + ctx->propagator->fill(unique_ptr(fill->fill(frameNo, exps))); ctx->propagator->fill(fill->rule); ctx->propagator->opacity(MULTIPLY(fill->opacity(frameNo), PP(ctx->propagator)->opacity)); if (ctx->propagator->strokeWidth() > 0) ctx->propagator->order(true); - - return nullptr; } static Shape* _draw(LottieGroup* parent, RenderContext* ctx) { - if (ctx->allowMerging && ctx->merging) return ctx->merging; + if (ctx->merging) return ctx->merging; auto shape = cast(ctx->propagator->duplicate()); ctx->merging = shape.get(); @@ -375,7 +439,7 @@ static void _repeat(LottieGroup* parent, unique_ptr path, RenderContext* } -static void _appendRect(Shape* shape, float x, float y, float w, float h, float r) +static void _appendRect(Shape* shape, float x, float y, float w, float h, float r, Matrix* transform) { //sharp rect if (mathZero(r)) { @@ -383,7 +447,11 @@ static void _appendRect(Shape* shape, float x, float y, float w, float h, float PathCommand::MoveTo, PathCommand::LineTo, PathCommand::LineTo, PathCommand::LineTo, PathCommand::Close }; + Point points[] = {{x + w, y}, {x + w, y + h}, {x, y + h}, {x, y}}; + if (transform) { + for (int i = 0; i < 4; i++) mathTransform(transform, &points[i]); + } shape->appendPath(commands, 5, points, 4); //round rect } else { @@ -401,7 +469,8 @@ static void _appendRect(Shape* shape, float x, float y, float w, float h, float auto hrx = rx * PATH_KAPPA; auto hry = ry * PATH_KAPPA; - Point points[] = { + constexpr int ptsCnt = 17; + Point points[ptsCnt] = { {x + w, y + ry}, //moveTo {x + w, y + h - ry}, //lineTo {x + w, y + h - ry + hry}, {x + w - rx + hrx, y + h}, {x + w - rx, y + h}, //cubicTo @@ -412,50 +481,52 @@ static void _appendRect(Shape* shape, float x, float y, float w, float h, float {x + w - rx, y}, //lineTo {x + w - rx + hrx, y}, {x + w, y + ry - hry}, {x + w, y + ry} //cubicTo }; - - shape->appendPath(commands, 10, points, 17); + + if (transform) { + for (int i = 0; i < ptsCnt; i++) mathTransform(transform, &points[i]); + } + shape->appendPath(commands, 10, points, ptsCnt); } } -static void _updateRect(LottieGroup* parent, LottieObject** child, float frameNo, TVG_UNUSED Inlist& contexts, RenderContext* ctx) +static void _updateRect(LottieGroup* parent, LottieObject** child, float frameNo, TVG_UNUSED Inlist& contexts, RenderContext* ctx, LottieExpressions* exps) { auto rect = static_cast(*child); - auto position = rect->position(frameNo); - auto size = rect->size(frameNo); - auto roundness = rect->radius(frameNo); + auto position = rect->position(frameNo, exps); + auto size = rect->size(frameNo, exps); + auto roundness = rect->radius(frameNo, exps); if (ctx->roundness > roundness) roundness = ctx->roundness; - if (roundness > 0.0f) { + if (roundness > ROUNDNESS_EPSILON) { if (roundness > size.x * 0.5f) roundness = size.x * 0.5f; if (roundness > size.y * 0.5f) roundness = size.y * 0.5f; } if (ctx->repeater) { auto path = Shape::gen(); - _appendRect(path.get(), position.x - size.x * 0.5f, position.y - size.y * 0.5f, size.x, size.y, roundness); + _appendRect(path.get(), position.x - size.x * 0.5f, position.y - size.y * 0.5f, size.x, size.y, roundness, ctx->transform); _repeat(parent, std::move(path), ctx); } else { auto merging = _draw(parent, ctx); - _appendRect(merging, position.x - size.x * 0.5f, position.y - size.y * 0.5f, size.x, size.y, roundness); - if (rect->direction == 2) merging->fill(FillRule::EvenOdd); + _appendRect(merging, position.x - size.x * 0.5f, position.y - size.y * 0.5f, size.x, size.y, roundness, ctx->transform); } } -static void _appendCircle(Shape* shape, float cx, float cy, float rx, float ry) +static void _appendCircle(Shape* shape, float cx, float cy, float rx, float ry, Matrix* transform) { auto rxKappa = rx * PATH_KAPPA; auto ryKappa = ry * PATH_KAPPA; - constexpr int commandsSize = 6; - PathCommand commands[commandsSize] = { + constexpr int cmdsCnt = 6; + PathCommand commands[cmdsCnt] = { PathCommand::MoveTo, PathCommand::CubicTo, PathCommand::CubicTo, PathCommand::CubicTo, PathCommand::CubicTo, PathCommand::Close }; - constexpr int pointsSize = 13; - Point points[pointsSize] = { + constexpr int ptsCnt = 13; + Point points[ptsCnt] = { {cx, cy - ry}, //moveTo {cx + rxKappa, cy - ry}, {cx + rx, cy - ryKappa}, {cx + rx, cy}, //cubicTo {cx + rx, cy + ryKappa}, {cx + rxKappa, cy + ry}, {cx, cy + ry}, //cubicTo @@ -463,51 +534,50 @@ static void _appendCircle(Shape* shape, float cx, float cy, float rx, float ry) {cx - rx, cy - ryKappa}, {cx - rxKappa, cy - ry}, {cx, cy - ry} //cubicTo }; - shape->appendPath(commands, commandsSize, points, pointsSize); + if (transform) { + for (int i = 0; i < ptsCnt; ++i) mathTransform(transform, &points[i]); + } + + shape->appendPath(commands, cmdsCnt, points, ptsCnt); } -static void _updateEllipse(LottieGroup* parent, LottieObject** child, float frameNo, TVG_UNUSED Inlist& contexts, RenderContext* ctx) + +static void _updateEllipse(LottieGroup* parent, LottieObject** child, float frameNo, TVG_UNUSED Inlist& contexts, RenderContext* ctx, LottieExpressions* exps) { auto ellipse = static_cast(*child); - auto position = ellipse->position(frameNo); - auto size = ellipse->size(frameNo); + auto position = ellipse->position(frameNo, exps); + auto size = ellipse->size(frameNo, exps); if (ctx->repeater) { auto path = Shape::gen(); - _appendCircle(path.get(), position.x, position.y, size.x * 0.5f, size.y * 0.5f); + _appendCircle(path.get(), position.x, position.y, size.x * 0.5f, size.y * 0.5f, ctx->transform); _repeat(parent, std::move(path), ctx); } else { auto merging = _draw(parent, ctx); - _appendCircle(merging, position.x, position.y, size.x * 0.5f, size.y * 0.5f); - if (ellipse->direction == 2) merging->fill(FillRule::EvenOdd); + _appendCircle(merging, position.x, position.y, size.x * 0.5f, size.y * 0.5f, ctx->transform); } } -static void _updatePath(LottieGroup* parent, LottieObject** child, float frameNo, TVG_UNUSED Inlist& contexts, RenderContext* ctx) +static void _updatePath(LottieGroup* parent, LottieObject** child, float frameNo, TVG_UNUSED Inlist& contexts, RenderContext* ctx, LottieExpressions* exps) { auto path = static_cast(*child); if (ctx->repeater) { auto p = Shape::gen(); - path->pathset(frameNo, P(p)->rs.path.cmds, P(p)->rs.path.pts); + path->pathset(frameNo, P(p)->rs.path.cmds, P(p)->rs.path.pts, ctx->transform, ctx->roundness, exps); _repeat(parent, std::move(p), ctx); } else { auto merging = _draw(parent, ctx); - if (path->pathset(frameNo, P(merging)->rs.path.cmds, P(merging)->rs.path.pts)) { + if (path->pathset(frameNo, P(merging)->rs.path.cmds, P(merging)->rs.path.pts, ctx->transform, ctx->roundness, exps)) { P(merging)->update(RenderUpdateFlag::Path); } - if (ctx->roundness > 1.0f && P(merging)->rs.stroke) { - TVGERR("LOTTIE", "FIXME: Path roundesss should be applied properly!"); - P(merging)->rs.stroke->join = StrokeJoin::Round; - } - if (path->direction == 2) merging->fill(FillRule::EvenOdd); } } -static void _updateText(LottieGroup* parent, LottieObject** child, float frameNo, TVG_UNUSED Inlist& contexts, TVG_UNUSED RenderContext* ctx) +static void _updateText(LottieGroup* parent, LottieObject** child, float frameNo, TVG_UNUSED Inlist& contexts, TVG_UNUSED RenderContext* ctx, LottieExpressions* exps) { auto text = static_cast(*child); auto& doc = text->doc(frameNo); @@ -519,9 +589,36 @@ static void _updateText(LottieGroup* parent, LottieObject** child, float frameNo float spacing = text->spacing(frameNo) / scale; Point cursor = {0.0f, 0.0f}; auto scene = Scene::gen(); + int line = 0; //text string - while (*p != '\0') { + while (true) { + //TODO: remove nested scenes. + //end of text, new line of the cursor position + if (*p == 13 || *p == 3 || *p == '\0') { + //text layout position + auto ascent = text->font->ascent * scale; + if (ascent > doc.bbox.size.y) ascent = doc.bbox.size.y; + Point layout = {doc.bbox.pos.x, doc.bbox.pos.y + ascent - doc.shift}; + + //adjust the layout + if (doc.justify == 1) layout.x += doc.bbox.size.x - (cursor.x * scale); //right aligned + else if (doc.justify == 2) layout.x += (doc.bbox.size.x * 0.5f) - (cursor.x * 0.5f * scale); //center aligned + + scene->translate(layout.x, layout.y); + scene->scale(scale); + + parent->scene->push(std::move(scene)); + + if (*p == '\0') break; + ++p; + + //new text group, single scene for each line + scene = Scene::gen(); + cursor.x = 0.0f; + cursor.y = ++line * (doc.height / scale); + } + //find the glyph for (auto g = text->font->chars.begin(); g < text->font->chars.end(); ++g) { auto glyph = *g; @@ -532,7 +629,7 @@ static void _updateText(LottieGroup* parent, LottieObject** child, float frameNo for (auto g = glyph->children.begin(); g < glyph->children.end(); ++g) { auto group = static_cast(*g); for (auto p = group->children.begin(); p < group->children.end(); ++p) { - if (static_cast(*p)->pathset(frameNo, P(shape)->rs.path.cmds, P(shape)->rs.path.pts)) { + if (static_cast(*p)->pathset(frameNo, P(shape)->rs.path.cmds, P(shape)->rs.path.pts, nullptr, 0.0f, exps)) { P(shape)->update(RenderUpdateFlag::Path); } } @@ -548,49 +645,91 @@ static void _updateText(LottieGroup* parent, LottieObject** child, float frameNo scene->push(std::move(shape)); - p += glyph->len; + p += glyph->len; - //end of text, new line of the cursor position - if (*p == 13 || *p == 3) { - cursor.x = 0.0f; - cursor.y += (doc.height / scale); - ++p; //advance the cursor position horizontally - } else { - cursor.x += glyph->width + spacing; - } + cursor.x += glyph->width + spacing + doc.tracking; break; } } } - - //text layout position - auto ascent = text->font->ascent * scale; - if (ascent > doc.bbox.size.y) ascent = doc.bbox.size.y; - Point layout = {doc.bbox.pos.x, doc.bbox.pos.y + ascent - doc.shift}; - - //adjust the layout - if (doc.justify == 1) layout.x += doc.bbox.size.x - (cursor.x * scale); //right aligned - else if (doc.justify == 2) layout.x += (doc.bbox.size.x * 0.5f) - (cursor.x * 0.5f * scale); //center aligned - - scene->translate(layout.x, layout.y); - scene->scale(scale); - - parent->scene->push(std::move(scene)); } -static void _updateStar(LottieGroup* parent, LottiePolyStar* star, Matrix* transform, float frameNo, Shape* merging) +static void _applyRoundedCorner(Shape* star, Shape* merging, float outerRoundness, float roundness, bool hasRoundness) +{ + static constexpr auto ROUNDED_POLYSTAR_MAGIC_NUMBER = 0.47829f; + + auto cmdCnt = star->pathCommands(nullptr); + const Point *pts = nullptr; + auto ptsCnt = star->pathCoords(&pts); + + auto len = mathLength(pts[1] - pts[2]); + auto r = len > 0.0f ? ROUNDED_POLYSTAR_MAGIC_NUMBER * mathMin(len * 0.5f, roundness) / len : 0.0f; + + if (hasRoundness) { + P(merging)->rs.path.cmds.grow((uint32_t)(1.5 * cmdCnt)); + P(merging)->rs.path.pts.grow((uint32_t)(4.5 * cmdCnt)); + + int start = 3 * mathZero(outerRoundness); + merging->moveTo(pts[start].x, pts[start].y); + + for (uint32_t i = 1 + start; i < ptsCnt; i += 6) { + auto& prev = pts[i]; + auto& curr = pts[i + 2]; + auto& next = (i < ptsCnt - start) ? pts[i + 4] : pts[2]; + auto& nextCtrl = (i < ptsCnt - start) ? pts[i + 5] : pts[3]; + auto dNext = r * (curr - next); + auto dPrev = r * (curr - prev); + + auto p0 = curr - 2.0f * dPrev; + auto p1 = curr - dPrev; + auto p2 = curr - dNext; + auto p3 = curr - 2.0f * dNext; + + merging->cubicTo(prev.x, prev.y, p0.x, p0.y, p0.x, p0.y); + merging->cubicTo(p1.x, p1.y, p2.x, p2.y, p3.x, p3.y); + merging->cubicTo(p3.x, p3.y, next.x, next.y, nextCtrl.x, nextCtrl.y); + } + } else { + P(merging)->rs.path.cmds.grow(2 * cmdCnt); + P(merging)->rs.path.pts.grow(4 * cmdCnt); + + auto dPrev = r * (pts[1] - pts[0]); + auto p = pts[0] + 2.0f * dPrev; + merging->moveTo(p.x, p.y); + + for (uint32_t i = 1; i < ptsCnt; ++i) { + auto& curr = pts[i]; + auto& next = (i == ptsCnt - 1) ? pts[1] : pts[i + 1]; + auto dNext = r * (curr - next); + + auto p0 = curr - 2.0f * dPrev; + auto p1 = curr - dPrev; + auto p2 = curr - dNext; + auto p3 = curr - 2.0f * dNext; + + merging->lineTo(p0.x, p0.y); + merging->cubicTo(p1.x, p1.y, p2.x, p2.y, p3.x, p3.y); + + dPrev = -1.0f * dNext; + } + } + merging->close(); +} + + +static void _updateStar(LottieGroup* parent, LottiePolyStar* star, Matrix* transform, float roundness, float frameNo, Shape* merging, LottieExpressions* exps) { static constexpr auto POLYSTAR_MAGIC_NUMBER = 0.47829f / 0.28f; - auto ptsCnt = star->ptsCnt(frameNo); - auto innerRadius = star->innerRadius(frameNo); - auto outerRadius = star->outerRadius(frameNo); - auto innerRoundness = star->innerRoundness(frameNo) * 0.01f; - auto outerRoundness = star->outerRoundness(frameNo) * 0.01f; + auto ptsCnt = star->ptsCnt(frameNo, exps); + auto innerRadius = star->innerRadius(frameNo, exps); + auto outerRadius = star->outerRadius(frameNo, exps); + auto innerRoundness = star->innerRoundness(frameNo, exps) * 0.01f; + auto outerRoundness = star->outerRoundness(frameNo, exps) * 0.01f; - auto angle = -90.0f * MATH_PI / 180.0f; + auto angle = mathDeg2Rad(-90.0f); auto partialPointRadius = 0.0f; auto anglePerPoint = (2.0f * MATH_PI / ptsCnt); auto halfAnglePerPoint = anglePerPoint * 0.5f; @@ -599,6 +738,9 @@ static void _updateStar(LottieGroup* parent, LottiePolyStar* star, Matrix* trans auto numPoints = size_t(ceilf(ptsCnt) * 2); auto direction = (star->direction == 0) ? 1.0f : -1.0f; auto hasRoundness = false; + bool roundedCorner = (roundness > ROUNDNESS_EPSILON) && (mathZero(innerRoundness) || mathZero(outerRoundness)); + //TODO: we can use PathCommand / PathCoord directly. + auto shape = roundedCorner ? Shape::gen().release() : merging; float x, y; @@ -618,17 +760,17 @@ static void _updateStar(LottieGroup* parent, LottiePolyStar* star, Matrix* trans } if (mathZero(innerRoundness) && mathZero(outerRoundness)) { - P(merging)->rs.path.pts.reserve(numPoints + 2); - P(merging)->rs.path.cmds.reserve(numPoints + 3); + P(shape)->rs.path.pts.reserve(numPoints + 2); + P(shape)->rs.path.cmds.reserve(numPoints + 3); } else { - P(merging)->rs.path.pts.reserve(numPoints * 3 + 2); - P(merging)->rs.path.cmds.reserve(numPoints + 3); + P(shape)->rs.path.pts.reserve(numPoints * 3 + 2); + P(shape)->rs.path.cmds.reserve(numPoints + 3); hasRoundness = true; } Point in = {x, y}; if (transform) mathTransform(transform, &in); - merging->moveTo(in.x, in.y); + shape->moveTo(in.x, in.y); for (size_t i = 0; i < numPoints; i++) { auto radius = longSegment ? outerRadius : innerRadius; @@ -676,28 +818,33 @@ static void _updateStar(LottieGroup* parent, LottiePolyStar* star, Matrix* trans mathTransform(transform, &in3); mathTransform(transform, &in4); } - merging->cubicTo(in2.x, in2.y, in3.x, in3.y, in4.x, in4.y); + shape->cubicTo(in2.x, in2.y, in3.x, in3.y, in4.x, in4.y); } else { Point in = {x, y}; if (transform) mathTransform(transform, &in); - merging->lineTo(in.x, in.y); + shape->lineTo(in.x, in.y); } angle += dTheta * direction; longSegment = !longSegment; } - merging->close(); + shape->close(); + + if (roundedCorner) { + _applyRoundedCorner(shape, merging, outerRoundness, roundness, hasRoundness); + delete(shape); + } } -static void _updatePolygon(LottieGroup* parent, LottiePolyStar* star, Matrix* transform, float frameNo, Shape* merging) +static void _updatePolygon(LottieGroup* parent, LottiePolyStar* star, Matrix* transform, float frameNo, Shape* merging, LottieExpressions* exps) { static constexpr auto POLYGON_MAGIC_NUMBER = 0.25f; - auto ptsCnt = size_t(floor(star->ptsCnt(frameNo))); - auto radius = star->outerRadius(frameNo); - auto roundness = star->outerRoundness(frameNo) * 0.01f; + auto ptsCnt = size_t(floor(star->ptsCnt(frameNo, exps))); + auto radius = star->outerRadius(frameNo, exps); + auto roundness = star->outerRoundness(frameNo, exps) * 0.01f; - auto angle = -90.0f * MATH_PI / 180.0f; + auto angle = mathDeg2Rad(-90.0f); auto anglePerPoint = 2.0f * MATH_PI / float(ptsCnt); auto direction = (star->direction == 0) ? 1.0f : -1.0f; auto hasRoundness = false; @@ -758,30 +905,31 @@ static void _updatePolygon(LottieGroup* parent, LottiePolyStar* star, Matrix* tr } -static void _updatePolystar(LottieGroup* parent, LottieObject** child, float frameNo, TVG_UNUSED Inlist& contexts, RenderContext* ctx) +static void _updatePolystar(LottieGroup* parent, LottieObject** child, float frameNo, TVG_UNUSED Inlist& contexts, RenderContext* ctx, LottieExpressions* exps) { auto star= static_cast(*child); //Optimize: Can we skip the individual coords transform? Matrix matrix; mathIdentity(&matrix); - auto position = star->position(frameNo); + auto position = star->position(frameNo, exps); mathTranslate(&matrix, position.x, position.y); - mathRotate(&matrix, star->rotation(frameNo)); + mathRotate(&matrix, star->rotation(frameNo, exps)); + + if (ctx->transform) matrix = mathMultiply(ctx->transform, &matrix); auto identity = mathIdentity((const Matrix*)&matrix); if (ctx->repeater) { auto p = Shape::gen(); - if (star->type == LottiePolyStar::Star) _updateStar(parent, star, identity ? nullptr : &matrix, frameNo, p.get()); - else _updatePolygon(parent, star, identity ? nullptr : &matrix, frameNo, p.get()); + if (star->type == LottiePolyStar::Star) _updateStar(parent, star, identity ? nullptr : &matrix, ctx->roundness, frameNo, p.get(), exps); + else _updatePolygon(parent, star, identity ? nullptr : &matrix, frameNo, p.get(), exps); _repeat(parent, std::move(p), ctx); } else { auto merging = _draw(parent, ctx); - if (star->type == LottiePolyStar::Star) _updateStar(parent, star, identity ? nullptr : &matrix, frameNo, merging); - else _updatePolygon(parent, star, identity ? nullptr : &matrix, frameNo, merging); + if (star->type == LottiePolyStar::Star) _updateStar(parent, star, identity ? nullptr : &matrix, ctx->roundness, frameNo, merging, exps); + else _updatePolygon(parent, star, identity ? nullptr : &matrix, frameNo, merging, exps); P(merging)->update(RenderUpdateFlag::Path); - if (star->direction == 2) merging->fill(FillRule::EvenOdd); } } @@ -825,28 +973,27 @@ static void _updateImage(LottieGroup* parent, LottieObject** child, float frameN } -static void _updateRoundedCorner(TVG_UNUSED LottieGroup* parent, LottieObject** child, float frameNo, TVG_UNUSED Inlist& contexts, RenderContext* ctx) +static void _updateRoundedCorner(TVG_UNUSED LottieGroup* parent, LottieObject** child, float frameNo, TVG_UNUSED Inlist& contexts, RenderContext* ctx, LottieExpressions* exps) { auto roundedCorner= static_cast(*child); - - auto roundness = roundedCorner->radius(frameNo); + auto roundness = roundedCorner->radius(frameNo, exps); if (ctx->roundness < roundness) ctx->roundness = roundness; } -static void _updateRepeater(TVG_UNUSED LottieGroup* parent, LottieObject** child, float frameNo, TVG_UNUSED Inlist& contexts, RenderContext* ctx) +static void _updateRepeater(TVG_UNUSED LottieGroup* parent, LottieObject** child, float frameNo, TVG_UNUSED Inlist& contexts, RenderContext* ctx, LottieExpressions* exps) { auto repeater= static_cast(*child); if (!ctx->repeater) ctx->repeater = new RenderRepeater(); - ctx->repeater->cnt = static_cast(repeater->copies(frameNo)); - ctx->repeater->offset = repeater->offset(frameNo); - ctx->repeater->position = repeater->position(frameNo); - ctx->repeater->anchor = repeater->anchor(frameNo); - ctx->repeater->scale = repeater->scale(frameNo); - ctx->repeater->rotation = repeater->rotation(frameNo); - ctx->repeater->startOpacity = repeater->startOpacity(frameNo); - ctx->repeater->endOpacity = repeater->endOpacity(frameNo); + ctx->repeater->cnt = static_cast(repeater->copies(frameNo, exps)); + ctx->repeater->offset = repeater->offset(frameNo, exps); + ctx->repeater->position = repeater->position(frameNo, exps); + ctx->repeater->anchor = repeater->anchor(frameNo, exps); + ctx->repeater->scale = repeater->scale(frameNo, exps); + ctx->repeater->rotation = repeater->rotation(frameNo, exps); + ctx->repeater->startOpacity = repeater->startOpacity(frameNo, exps); + ctx->repeater->endOpacity = repeater->endOpacity(frameNo, exps); ctx->repeater->inorder = repeater->inorder; ctx->repeater->interpOpacity = (ctx->repeater->startOpacity == ctx->repeater->endOpacity) ? false : true; @@ -854,12 +1001,12 @@ static void _updateRepeater(TVG_UNUSED LottieGroup* parent, LottieObject** child } -static void _updateTrimpath(TVG_UNUSED LottieGroup* parent, LottieObject** child, float frameNo, TVG_UNUSED Inlist& contexts, RenderContext* ctx) +static void _updateTrimpath(TVG_UNUSED LottieGroup* parent, LottieObject** child, float frameNo, TVG_UNUSED Inlist& contexts, RenderContext* ctx, LottieExpressions* exps) { auto trimpath= static_cast(*child); float begin, end; - trimpath->segment(frameNo, begin, end); + trimpath->segment(frameNo, begin, end, exps); if (P(ctx->propagator)->rs.stroke) { auto pbegin = P(ctx->propagator)->rs.stroke->trim.begin; @@ -869,13 +1016,11 @@ static void _updateTrimpath(TVG_UNUSED LottieGroup* parent, LottieObject** child end = (length * end) + pbegin; } - P(ctx->propagator)->strokeTrim(begin, end); - - if (trimpath->type == LottieTrimpath::Individual) ctx->allowMerging = false; + P(ctx->propagator)->strokeTrim(begin, end, trimpath->type == LottieTrimpath::Type::Individual ? true : false); } -static void _updateChildren(LottieGroup* parent, float frameNo, Inlist& contexts) +static void _updateChildren(LottieGroup* parent, float frameNo, Inlist& contexts, LottieExpressions* exps) { contexts.head->begin = parent->children.end() - 1; @@ -883,45 +1028,46 @@ static void _updateChildren(LottieGroup* parent, float frameNo, InlistreqFragment = parent->reqFragment; for (auto child = ctx->begin; child >= parent->children.data; --child) { + //Here switch-case statements are more performant than virtual methods. switch ((*child)->type) { case LottieObject::Group: { - _updateGroup(parent, child, frameNo, contexts, ctx); + _updateGroup(parent, child, frameNo, contexts, ctx, exps); break; } case LottieObject::Transform: { - _updateTransform(parent, child, frameNo, contexts, ctx); + _updateTransform(parent, child, frameNo, contexts, ctx, exps); break; } case LottieObject::SolidFill: { - _updateSolidFill(parent, child, frameNo, contexts, ctx); + _updateSolidFill(parent, child, frameNo, contexts, ctx, exps); break; } case LottieObject::SolidStroke: { - _updateSolidStroke(parent, child, frameNo, contexts, ctx); + _updateSolidStroke(parent, child, frameNo, contexts, ctx, exps); break; } case LottieObject::GradientFill: { - _updateGradientFill(parent, child, frameNo, contexts, ctx); + _updateGradientFill(parent, child, frameNo, contexts, ctx, exps); break; } case LottieObject::GradientStroke: { - _updateGradientStroke(parent, child, frameNo, contexts, ctx); + _updateGradientStroke(parent, child, frameNo, contexts, ctx, exps); break; } case LottieObject::Rect: { - _updateRect(parent, child, frameNo, contexts, ctx); + _updateRect(parent, child, frameNo, contexts, ctx, exps); break; } case LottieObject::Ellipse: { - _updateEllipse(parent, child, frameNo, contexts, ctx); + _updateEllipse(parent, child, frameNo, contexts, ctx, exps); break; } case LottieObject::Path: { - _updatePath(parent, child, frameNo, contexts, ctx); + _updatePath(parent, child, frameNo, contexts, ctx, exps); break; } case LottieObject::Polystar: { - _updatePolystar(parent, child, frameNo, contexts, ctx); + _updatePolystar(parent, child, frameNo, contexts, ctx, exps); break; } case LottieObject::Image: { @@ -929,19 +1075,19 @@ static void _updateChildren(LottieGroup* parent, float frameNo, Inlistchildren.empty()) return; - frameNo = precomp->remap(frameNo); + frameNo = precomp->remap(frameNo, exps); for (auto child = precomp->children.end() - 1; child >= precomp->children.begin(); --child) { - _updateLayer(precomp, static_cast(*child), frameNo); + _updateLayer(precomp, static_cast(*child), frameNo, exps); } //clip the layer viewport @@ -986,7 +1132,7 @@ static void _updateSolid(LottieLayer* layer) } -static void _updateMaskings(LottieLayer* layer, float frameNo) +static void _updateMaskings(LottieLayer* layer, float frameNo, LottieExpressions* exps) { if (layer->masks.count == 0) return; @@ -999,7 +1145,7 @@ static void _updateMaskings(LottieLayer* layer, float frameNo) auto shape = Shape::gen().release(); shape->fill(255, 255, 255, mask->opacity(frameNo)); shape->transform(layer->cache.matrix); - if (mask->pathset(frameNo, P(shape)->rs.path.cmds, P(shape)->rs.path.pts)) { + if (mask->pathset(frameNo, P(shape)->rs.path.cmds, P(shape)->rs.path.pts, nullptr, 0.0f, exps)) { P(shape)->update(RenderUpdateFlag::Path); } auto method = mask->method; @@ -1024,12 +1170,12 @@ static void _updateMaskings(LottieLayer* layer, float frameNo) } -static bool _updateMatte(LottieLayer* root, LottieLayer* layer, float frameNo) +static bool _updateMatte(LottieLayer* root, LottieLayer* layer, float frameNo, LottieExpressions* exps) { auto target = layer->matte.target; if (!target) return true; - _updateLayer(root, target, frameNo); + _updateLayer(root, target, frameNo, exps); if (target->scene) { layer->scene->composite(cast(target->scene), layer->matte.type); @@ -1043,14 +1189,14 @@ static bool _updateMatte(LottieLayer* root, LottieLayer* layer, float frameNo) } -static void _updateLayer(LottieLayer* root, LottieLayer* layer, float frameNo) +static void _updateLayer(LottieLayer* root, LottieLayer* layer, float frameNo, LottieExpressions* exps) { layer->scene = nullptr; //visibility if (frameNo < layer->inFrame || frameNo >= layer->outFrame) return; - _updateTransform(layer, frameNo); + _updateTransform(layer, frameNo, exps); //full transparent scene. no need to perform if (layer->type != LottieLayer::Null && layer->cache.opacity == 0) return; @@ -1065,13 +1211,13 @@ static void _updateLayer(LottieLayer* root, LottieLayer* layer, float frameNo) if (layer->matte.target && layer->masks.count > 0) TVGERR("LOTTIE", "FIXME: Matte + Masking??"); - if (!_updateMatte(root, layer, frameNo)) return; + if (!_updateMatte(root, layer, frameNo, exps)) return; - _updateMaskings(layer, frameNo); + _updateMaskings(layer, frameNo, exps); switch (layer->type) { case LottieLayer::Precomp: { - _updatePrecomp(layer, frameNo); + _updatePrecomp(layer, frameNo, exps); break; } case LottieLayer::Solid: { @@ -1082,7 +1228,7 @@ static void _updateLayer(LottieLayer* root, LottieLayer* layer, float frameNo) if (!layer->children.empty()) { Inlist contexts; contexts.back(new RenderContext); - _updateChildren(layer, frameNo, contexts); + _updateChildren(layer, frameNo, contexts, exps); contexts.free(); } break; @@ -1109,7 +1255,6 @@ static void _buildReference(LottieComposition* comp, LottieLayer* layer) } else if (layer->type == LottieLayer::Image) { layer->children.push(*asset); } - layer->statical &= (*asset)->statical; break; } } @@ -1176,15 +1321,11 @@ static bool _buildComposition(LottieComposition* comp, LottieGroup* parent) _bulidHierarchy(parent, child->matte.target); //precomp referencing if (child->matte.target->refId) _buildReference(comp, child->matte.target); - child->statical &= child->matte.target->statical; } _bulidHierarchy(parent, child); //attach the necessary font data if (child->type == LottieLayer::Text) _attachFont(comp, child); - - child->statical &= parent->statical; - parent->statical &= child->statical; } return true; } @@ -1196,6 +1337,8 @@ static bool _buildComposition(LottieComposition* comp, LottieGroup* parent) bool LottieBuilder::update(LottieComposition* comp, float frameNo) { + if (comp->root->children.empty()) return false; + frameNo += comp->startFrame; if (frameNo < comp->startFrame) frameNo = comp->startFrame; if (frameNo >= comp->endFrame) frameNo = (comp->endFrame - 1); @@ -1204,9 +1347,12 @@ bool LottieBuilder::update(LottieComposition* comp, float frameNo) auto root = comp->root; root->scene->clear(); + if (exps && comp->expressions) exps->update(comp->timeAtFrame(frameNo)); + for (auto child = root->children.end() - 1; child >= root->children.begin(); --child) { - _updateLayer(root, static_cast(*child), frameNo); + _updateLayer(root, static_cast(*child), frameNo, exps); } + return true; } diff --git a/src/libs/thorvg/tvgLottieBuilder.h b/src/libs/thorvg/tvgLottieBuilder.h index 25160867a..690fa85ac 100644 --- a/src/libs/thorvg/tvgLottieBuilder.h +++ b/src/libs/thorvg/tvgLottieBuilder.h @@ -22,16 +22,28 @@ #include "../../lv_conf_internal.h" #if LV_USE_THORVG_INTERNAL - #ifndef _TVG_LOTTIE_BUILDER_H_ #define _TVG_LOTTIE_BUILDER_H_ #include "tvgCommon.h" +#include "tvgLottieExpressions.h" struct LottieComposition; struct LottieBuilder { + LottieExpressions* exps = nullptr; + + LottieBuilder() + { + exps = LottieExpressions::instance(); + } + + ~LottieBuilder() + { + LottieExpressions::retrieve(exps); + } + bool update(LottieComposition* comp, float progress); void build(LottieComposition* comp); }; diff --git a/src/libs/thorvg/tvgLottieExpressions.cpp b/src/libs/thorvg/tvgLottieExpressions.cpp new file mode 100644 index 000000000..466173c44 --- /dev/null +++ b/src/libs/thorvg/tvgLottieExpressions.cpp @@ -0,0 +1,1298 @@ +/* + * Copyright (c) 2024 the ThorVG project. All rights reserved. + + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#include "../../lv_conf_internal.h" +#if LV_USE_THORVG_INTERNAL + +#include "tvgMath.h" +#include "tvgLottieModel.h" +#include "tvgLottieExpressions.h" + +#ifdef THORVG_LOTTIE_EXPRESSIONS_SUPPORT + +/************************************************************************/ +/* Internal Class Implementation */ +/************************************************************************/ + +struct ExpContent +{ + LottieObject* obj; + float frameNo; +}; + + +//reserved expressions speicifiers +static const char* EXP_NAME = "name"; +static const char* EXP_CONTENT = "content"; +static const char* EXP_WIDTH = "width"; +static const char* EXP_HEIGHT = "height"; +static const char* EXP_CYCLE = "cycle"; +static const char* EXP_PINGPONG = "pingpong"; +static const char* EXP_OFFSET = "offset"; +static const char* EXP_CONTINUE = "continue"; +static const char* EXP_TIME = "time"; +static const char* EXP_VALUE = "value"; +static const char* EXP_INDEX = "index"; +static const char* EXP_EFFECT= "effect"; + +static LottieExpressions* exps = nullptr; //singleton instance engine + + +static void contentFree(void *native_p, struct jerry_object_native_info_t *info_p) +{ + free(native_p); +} + +static jerry_object_native_info_t freeCb {contentFree, 0, 0}; +static uint32_t engineRefCnt = 0; //Expressions Engine reference count + + +static char* _name(jerry_value_t args) +{ + auto arg0 = jerry_value_to_string(args); + auto len = jerry_string_length(arg0); + auto name = (jerry_char_t*)malloc(len * sizeof(jerry_char_t) + 1); + jerry_string_to_buffer(arg0, JERRY_ENCODING_UTF8, name, len); + name[len] = '\0'; + jerry_value_free(arg0); + return (char*) name; +} + + +static jerry_value_t _toComp(const jerry_call_info_t* info, const jerry_value_t args[], const jerry_length_t argsCnt) +{ + TVGERR("LOTTIE", "toComp is not supported in expressions!"); + + return jerry_undefined(); +} + + +static void _buildTransform(jerry_value_t context, LottieTransform* transform) +{ + if (!transform) return; + + auto obj = jerry_object(); + jerry_object_set_sz(context, "transform", obj); + + auto anchorPoint = jerry_object(); + jerry_object_set_native_ptr(anchorPoint, nullptr, &transform->anchor); + jerry_object_set_sz(obj, "anchorPoint", anchorPoint); + jerry_value_free(anchorPoint); + + auto position = jerry_object(); + jerry_object_set_native_ptr(position, nullptr, &transform->position); + jerry_object_set_sz(obj, "position", position); + jerry_value_free(position); + + auto scale = jerry_object(); + jerry_object_set_native_ptr(scale, nullptr, &transform->scale); + jerry_object_set_sz(obj, "scale", scale); + jerry_value_free(scale); + + auto rotation = jerry_object(); + jerry_object_set_native_ptr(rotation, nullptr, &transform->rotation); + jerry_object_set_sz(obj, "rotation", rotation); + jerry_value_free(rotation); + + auto opacity = jerry_object(); + jerry_object_set_native_ptr(opacity, nullptr, &transform->opacity); + jerry_object_set_sz(obj, "opacity", opacity); + jerry_value_free(opacity); + + jerry_value_free(obj); +} + + +static void _buildLayer(jerry_value_t context, LottieLayer* layer, LottieComposition* comp) +{ + auto width = jerry_number(layer->w); + jerry_object_set_sz(context, EXP_WIDTH, width); + jerry_value_free(width); + + auto height = jerry_number(layer->h); + jerry_object_set_sz(context, EXP_HEIGHT, height); + jerry_value_free(height); + + auto index = jerry_number(layer->id); + jerry_object_set_sz(context, EXP_INDEX, index); + jerry_value_free(index); + + auto parent = jerry_object(); + jerry_object_set_native_ptr(parent, nullptr, layer->parent); + jerry_object_set_sz(context, "parent", parent); + jerry_value_free(parent); + + auto hasParent = jerry_boolean(layer->parent ? true : false); + jerry_object_set_sz(context, "hasParent", hasParent); + jerry_value_free(hasParent); + + auto inPoint = jerry_number(layer->inFrame); + jerry_object_set_sz(context, "inPoint", inPoint); + jerry_value_free(inPoint); + + auto outPoint = jerry_number(layer->outFrame); + jerry_object_set_sz(context, "outPoint", outPoint); + jerry_value_free(outPoint); + + auto startTime = jerry_number(comp->timeAtFrame(layer->startFrame)); + jerry_object_set_sz(context, "startTime", startTime); + jerry_value_free(startTime); + + auto hasVideo = jerry_boolean(false); + jerry_object_set_sz(context, "hasVideo", hasVideo); + jerry_value_free(hasVideo); + + auto hasAudio = jerry_boolean(false); + jerry_object_set_sz(context, "hasAudio", hasAudio); + jerry_value_free(hasAudio); + + //active, #current in the animation range? + + auto enabled = jerry_boolean(!layer->hidden); + jerry_object_set_sz(context, "enabled", enabled); + jerry_value_free(enabled); + + auto audioActive = jerry_boolean(false); + jerry_object_set_sz(context, "audioActive", audioActive); + jerry_value_free(audioActive); + + //sampleImage(point, radius = [.5, .5], postEffect=true, t=time) + + _buildTransform(context, layer->transform); + + //audioLevels, #the value of the Audio Levels property of the layer in decibels + + auto timeRemap = jerry_object(); + jerry_object_set_native_ptr(timeRemap, nullptr, &layer->timeRemap); + jerry_object_set_sz(context, "timeRemap", timeRemap); + jerry_value_free(timeRemap); + + //marker.key(index) + //marker.key(name) + //marker.nearestKey(t) + //marker.numKeys + + auto name = jerry_string_sz(layer->name); + jerry_object_set_sz(context, EXP_NAME, name); + jerry_value_free(name); + + auto toComp = jerry_function_external(_toComp); + jerry_object_set_sz(context, "toComp", toComp); + jerry_object_set_native_ptr(toComp, nullptr, comp); + jerry_value_free(toComp); +} + + +static jerry_value_t _value(float frameNo, LottieExpression* exp) +{ + switch (exp->type) { + case LottieProperty::Type::Point: { + auto value = jerry_object(); + auto pos = (*static_cast(exp->property))(frameNo); + auto val1 = jerry_number(pos.x); + auto val2 = jerry_number(pos.y); + jerry_object_set_index(value, 0, val1); + jerry_object_set_index(value, 1, val2); + jerry_value_free(val1); + jerry_value_free(val2); + return value; + } + case LottieProperty::Type::Float: { + return jerry_number((*static_cast(exp->property))(frameNo)); + } + case LottieProperty::Type::Opacity: { + return jerry_number((*static_cast(exp->property))(frameNo)); + } + case LottieProperty::Type::PathSet: { + auto value = jerry_object(); + jerry_object_set_native_ptr(value, nullptr, exp->property); + return value; + } + case LottieProperty::Type::Position: { + auto value = jerry_object(); + auto pos = (*static_cast(exp->property))(frameNo); + auto val1 = jerry_number(pos.x); + auto val2 = jerry_number(pos.y); + jerry_object_set_index(value, 0, val1); + jerry_object_set_index(value, 1, val2); + jerry_value_free(val1); + jerry_value_free(val2); + return value; + } + default: { + TVGERR("LOTTIE", "Non supported type for value? = %d", (int) exp->type); + } + } + return jerry_undefined(); +} + + +static jerry_value_t _addsub(const jerry_value_t args[], float addsub) +{ + //1d + if (jerry_value_is_number(args[0])) return jerry_number(jerry_value_as_number(args[0]) + addsub * jerry_value_as_number(args[1])); + + //2d + auto val1 = jerry_object_get_index(args[0], 0); + auto val2 = jerry_object_get_index(args[0], 1); + auto val3 = jerry_object_get_index(args[1], 0); + auto val4 = jerry_object_get_index(args[1], 1); + auto x = jerry_value_as_number(val1) + addsub * jerry_value_as_number(val3); + auto y = jerry_value_as_number(val2) + addsub * jerry_value_as_number(val4); + + jerry_value_free(val1); + jerry_value_free(val2); + jerry_value_free(val3); + jerry_value_free(val4); + + auto obj = jerry_object(); + val1 = jerry_number(x); + val2 = jerry_number(y); + jerry_object_set_index(obj, 0, val1); + jerry_object_set_index(obj, 1, val2); + jerry_value_free(val1); + jerry_value_free(val2); + + return obj; +} + + +static jerry_value_t _muldiv(const jerry_value_t arg1, float arg2) +{ + //1d + if (jerry_value_is_number(arg1)) return jerry_number(jerry_value_as_number(arg1) * arg2); + + //2d + auto val1 = jerry_object_get_index(arg1, 0); + auto val2 = jerry_object_get_index(arg1, 1); + auto x = jerry_value_as_number(val1) * arg2; + auto y = jerry_value_as_number(val2) * arg2; + + jerry_value_free(val1); + jerry_value_free(val2); + + auto obj = jerry_object(); + val1 = jerry_number(x); + val2 = jerry_number(y); + jerry_object_set_index(obj, 0, val1); + jerry_object_set_index(obj, 1, val2); + jerry_value_free(val1); + jerry_value_free(val2); + + return obj; +} + + +static jerry_value_t _add(const jerry_call_info_t* info, const jerry_value_t args[], const jerry_length_t argsCnt) +{ + return _addsub(args, 1.0f); +} + + +static jerry_value_t _sub(const jerry_call_info_t* info, const jerry_value_t args[], const jerry_length_t argsCnt) +{ + return _addsub(args, -1.0f); +} + + +static jerry_value_t _mul(const jerry_call_info_t* info, const jerry_value_t args[], const jerry_length_t argsCnt) +{ + return _muldiv(args[0], jerry_value_as_number(args[1])); +} + + +static jerry_value_t _div(const jerry_call_info_t* info, const jerry_value_t args[], const jerry_length_t argsCnt) +{ + return _muldiv(args[0], 1.0f / jerry_value_as_number(args[1])); +} + + +static jerry_value_t _interp(float t, const jerry_value_t args[], int argsCnt) +{ + auto tMin = 0.0f; + auto tMax = 1.0f; + int idx = 0; + + if (argsCnt > 3) { + tMin = jerry_value_as_number(args[1]); + tMax = jerry_value_as_number(args[2]); + idx += 2; + } + + //2d + if (jerry_value_is_object(args[idx + 1]) && jerry_value_is_object(args[idx + 2])) { + auto val1 = jerry_object_get_index(args[0], 0); + auto val2 = jerry_object_get_index(args[0], 1); + auto val3 = jerry_object_get_index(args[1], 0); + auto val4 = jerry_object_get_index(args[1], 1); + + Point pt1 = {(float)jerry_value_as_number(val1), (float)jerry_value_as_number(val2)}; + Point pt2 = {(float)jerry_value_as_number(val3), (float)jerry_value_as_number(val4)}; + Point ret; + if (t <= tMin) ret = pt1; + else if (t >= tMax) ret = pt2; + else ret = mathLerp(pt1, pt2, t); + + jerry_value_free(val1); + jerry_value_free(val2); + jerry_value_free(val3); + jerry_value_free(val4); + + auto obj = jerry_object(); + val1 = jerry_number(ret.x); + val2 = jerry_number(ret.y); + jerry_object_set_index(obj, 0, val1); + jerry_object_set_index(obj, 1, val2); + jerry_value_free(val1); + jerry_value_free(val2); + + return obj; + } + + //1d + auto val1 = (float) jerry_value_as_number(args[idx + 1]); + if (t <= tMin) jerry_number(val1); + auto val2 = (float) jerry_value_as_number(args[idx + 2]); + if (t >= tMax) jerry_number(val2); + return jerry_number(mathLerp(val1, val2, t)); +} + + +static jerry_value_t _linear(const jerry_call_info_t* info, const jerry_value_t args[], const jerry_length_t argsCnt) +{ + auto t = (float) jerry_value_as_number(args[0]); + return _interp(t, args, jerry_value_as_uint32(argsCnt)); +} + + +static jerry_value_t _ease(const jerry_call_info_t* info, const jerry_value_t args[], const jerry_length_t argsCnt) +{ + auto t = (float) jerry_value_as_number(args[0]); + t = (t < 0.5) ? (4 * t * t * t) : (1.0f - pow(-2.0f * t + 2.0f, 3) * 0.5f); + return _interp(t, args, jerry_value_as_uint32(argsCnt)); +} + + + +static jerry_value_t _easeIn(const jerry_call_info_t* info, const jerry_value_t args[], const jerry_length_t argsCnt) +{ + auto t = (float) jerry_value_as_number(args[0]); + t = t * t * t; + return _interp(t, args, jerry_value_as_uint32(argsCnt)); +} + + +static jerry_value_t _easeOut(const jerry_call_info_t* info, const jerry_value_t args[], const jerry_length_t argsCnt) +{ + auto t = (float) jerry_value_as_number(args[0]); + t = 1.0f - pow(1.0f - t, 3); + return _interp(t, args, jerry_value_as_uint32(argsCnt)); +} + + +static jerry_value_t _clamp(const jerry_call_info_t* info, const jerry_value_t args[], const jerry_length_t argsCnt) +{ + auto num = jerry_value_as_number(args[0]); + auto limit1 = jerry_value_as_number(args[1]); + auto limit2 = jerry_value_as_number(args[2]); + + //clamping + if (num < limit1) num = limit1; + if (num > limit2) num = limit2; + + return jerry_number(num); +} + + +static jerry_value_t _dot(const jerry_call_info_t* info, const jerry_value_t args[], const jerry_length_t argsCnt) +{ + auto val1 = jerry_object_get_index(args[0], 0); + auto val2 = jerry_object_get_index(args[0], 1); + auto val3 = jerry_object_get_index(args[1], 0); + auto val4 = jerry_object_get_index(args[1], 1); + + auto x = jerry_value_as_number(val1) * jerry_value_as_number(val3); + auto y = jerry_value_as_number(val2) * jerry_value_as_number(val4); + + jerry_value_free(val1); + jerry_value_free(val2); + jerry_value_free(val3); + jerry_value_free(val4); + + return jerry_number(x + y); +} + + +static jerry_value_t _cross(const jerry_call_info_t* info, const jerry_value_t args[], const jerry_length_t argsCnt) +{ + auto val1 = jerry_object_get_index(args[0], 0); + auto val2 = jerry_object_get_index(args[0], 1); + auto val3 = jerry_object_get_index(args[1], 0); + auto val4 = jerry_object_get_index(args[1], 1); + + auto x = jerry_value_as_number(val1) * jerry_value_as_number(val4); + auto y = jerry_value_as_number(val2) * jerry_value_as_number(val3); + + jerry_value_free(val1); + jerry_value_free(val2); + jerry_value_free(val3); + jerry_value_free(val4); + + return jerry_number(x - y); +} + + +static jerry_value_t _normalize(const jerry_call_info_t* info, const jerry_value_t args[], const jerry_length_t argsCnt) +{ + auto val1 = jerry_object_get_index(args[0], 0); + auto val2 = jerry_object_get_index(args[0], 1); + auto x = jerry_value_as_number(val1); + auto y = jerry_value_as_number(val2); + + jerry_value_free(val1); + jerry_value_free(val2); + + auto length = sqrtf(x * x + y * y); + + x /= length; + y /= length; + + auto obj = jerry_object(); + val1 = jerry_number(x); + val2 = jerry_number(y); + jerry_object_set_index(obj, 0, val1); + jerry_object_set_index(obj, 0, val2); + jerry_value_free(val1); + jerry_value_free(val2); + + return obj; +} + + +static jerry_value_t _length(const jerry_call_info_t* info, const jerry_value_t args[], const jerry_length_t argsCnt) +{ + auto val1 = jerry_object_get_index(args[0], 0); + auto val2 = jerry_object_get_index(args[0], 1); + auto x = jerry_value_as_number(val1); + auto y = jerry_value_as_number(val2); + + jerry_value_free(val1); + jerry_value_free(val2); + + return jerry_number(sqrtf(x * x + y * y)); +} + + +static jerry_value_t _random(const jerry_call_info_t* info, const jerry_value_t args[], const jerry_length_t argsCnt) +{ + auto val = (float)(rand() % 10000001); + return jerry_number(val * 0.0000001f); +} + + +static jerry_value_t _deg2rad(const jerry_call_info_t* info, const jerry_value_t args[], const jerry_length_t argsCnt) +{ + return jerry_number(mathDeg2Rad((float)jerry_value_as_number(args[0]))); +} + + +static jerry_value_t _rad2deg(const jerry_call_info_t* info, const jerry_value_t args[], const jerry_length_t argsCnt) +{ + return jerry_number(mathRad2Deg((float)jerry_value_as_number(args[0]))); +} + + +static jerry_value_t _effect(const jerry_call_info_t* info, const jerry_value_t args[], const jerry_length_t argsCnt) +{ + TVGERR("LOTTIE", "effect is not supported in expressions!"); + + return jerry_undefined(); +} + + +static jerry_value_t _fromCompToSurface(const jerry_call_info_t* info, const jerry_value_t args[], const jerry_length_t argsCnt) +{ + TVGERR("LOTTIE", "fromCompToSurface is not supported in expressions!"); + + return jerry_undefined(); +} + + +static jerry_value_t _content(const jerry_call_info_t* info, const jerry_value_t args[], const jerry_length_t argsCnt) +{ + auto name = _name(args[0]); + auto data = static_cast(jerry_object_get_native_ptr(info->function, &freeCb)); + auto group = static_cast(data->obj); + auto target = group->content((char*)name); + free(name); + if (!target) return jerry_undefined(); + + //find the a path property(sh) in the group layer? + switch (target->type) { + case LottieObject::Group: { + auto group = static_cast(target); + auto obj = jerry_function_external(_content); + + //attach a transform + for (auto c = group->children.begin(); c < group->children.end(); ++c) { + if ((*c)->type == LottieObject::Type::Transform) { + _buildTransform(obj, static_cast(*c)); + break; + } + } + auto data2 = (ExpContent*)malloc(sizeof(ExpContent)); + data2->obj = group; + data2->frameNo = data->frameNo; + jerry_object_set_native_ptr(obj, &freeCb, data2); + jerry_object_set_sz(obj, EXP_CONTENT, obj); + return obj; + } + case LottieObject::Path: { + jerry_value_t obj = jerry_object(); + jerry_object_set_native_ptr(obj, nullptr, &static_cast(target)->pathset); + jerry_object_set_sz(obj, "path", obj); + return obj; + } + case LottieObject::Trimpath: { + auto trimpath = static_cast(target); + jerry_value_t obj = jerry_object(); + auto start = jerry_number(trimpath->start(data->frameNo)); + jerry_object_set_sz(obj, "start", start); + jerry_value_free(start); + auto end = jerry_number(trimpath->end(data->frameNo)); + jerry_object_set_sz(obj, "end", end); + jerry_value_free(end); + auto offset = jerry_number(trimpath->offset(data->frameNo)); + jerry_object_set_sz(obj, "offset", end); + jerry_value_free(offset); + return obj; + } + default: break; + } + return jerry_undefined(); +} + + +static jerry_value_t _layer(const jerry_call_info_t* info, const jerry_value_t args[], const jerry_length_t argsCnt) +{ + auto comp = static_cast(jerry_object_get_native_ptr(info->function, nullptr)); + LottieLayer* layer; + + //layer index + if (jerry_value_is_number(args[0])) { + auto idx = (uint16_t)jerry_value_as_int32(args[0]); + layer = comp->layer(idx); + jerry_value_free(idx); + //layer name + } else { + auto name = _name(args[0]); + layer = comp->layer((char*)name); + free(name); + } + + if (!layer) return jerry_undefined(); + + auto obj = jerry_object(); + jerry_object_set_native_ptr(obj, nullptr, layer); + _buildLayer(obj, layer, comp); + + return obj; +} + + +static jerry_value_t _nearestKey(const jerry_call_info_t* info, const jerry_value_t args[], const jerry_length_t argsCnt) +{ + auto exp = static_cast(jerry_object_get_native_ptr(info->function, nullptr)); + auto time = jerry_value_as_number(args[0]); + auto frameNo = exp->comp->frameAtTime(time); + auto index = jerry_number(exp->property->nearest(frameNo)); + + auto obj = jerry_object(); + jerry_object_set_sz(obj, EXP_INDEX, index); + jerry_value_free(index); + + return obj; +} + + +static jerry_value_t _valueAtTime(const jerry_call_info_t* info, const jerry_value_t args[], const jerry_length_t argsCnt) +{ + auto exp = static_cast(jerry_object_get_native_ptr(info->function, nullptr)); + auto time = jerry_value_as_number(args[0]); + auto frameNo = exp->comp->frameAtTime(time); + return _value(frameNo, exp); +} + + +static jerry_value_t _velocityAtTime(const jerry_call_info_t* info, const jerry_value_t args[], const jerry_length_t argsCnt) +{ + auto exp = static_cast(jerry_object_get_native_ptr(info->function, nullptr)); + auto time = jerry_value_as_number(args[0]); + auto frameNo = exp->comp->frameAtTime(time); + auto key = exp->property->nearest(frameNo); + auto pframe = exp->property->frameNo(key - 1); + auto cframe = exp->property->frameNo(key); + auto elapsed = (cframe - pframe) / (exp->comp->frameRate); + + Point cur, prv; + + //compute the velocity + switch (exp->type) { + case LottieProperty::Type::Point: { + prv = (*static_cast(exp->property))(pframe); + cur = (*static_cast(exp->property))(cframe); + break; + } + case LottieProperty::Type::Position: { + prv = (*static_cast(exp->property))(pframe); + cur = (*static_cast(exp->property))(cframe); + break; + } + default: { + TVGERR("LOTTIE", "Non supported type for velocityAtTime?"); + return jerry_undefined(); + } + } + + float velocity[] = {(cur.x - prv.x) / elapsed, (cur.y - prv.y) / elapsed}; + + auto obj = jerry_object(); + auto val1 = jerry_number(velocity[0]); + auto val2 = jerry_number(velocity[1]); + jerry_object_set_index(obj, 0, val1); + jerry_object_set_index(obj, 1, val2); + jerry_value_free(val1); + jerry_value_free(val2); + + return obj; +} + + +static jerry_value_t _speedAtTime(const jerry_call_info_t* info, const jerry_value_t args[], const jerry_length_t argsCnt) +{ + auto exp = static_cast(jerry_object_get_native_ptr(info->function, nullptr)); + auto time = jerry_value_as_number(args[0]); + auto frameNo = exp->comp->frameAtTime(time); + auto key = exp->property->nearest(frameNo); + auto pframe = exp->property->frameNo(key - 1); + auto cframe = exp->property->frameNo(key); + auto elapsed = (cframe - pframe) / (exp->comp->frameRate); + + Point cur, prv; + + //compute the velocity + switch (exp->type) { + case LottieProperty::Type::Point: { + prv = (*static_cast(exp->property))(pframe); + cur = (*static_cast(exp->property))(cframe); + break; + } + case LottieProperty::Type::Position: { + prv = (*static_cast(exp->property))(pframe); + cur = (*static_cast(exp->property))(cframe); + break; + } + default: { + TVGERR("LOTTIE", "Non supported type for speedAtTime?"); + return jerry_undefined(); + } + } + + auto speed = sqrtf(pow(cur.x - prv.x, 2) + pow(cur.y - prv.y, 2)) / elapsed; + auto obj = jerry_number(speed); + return obj; +} + + +static bool _loopOutCommon(LottieExpression* exp, const jerry_value_t args[], const jerry_length_t argsCnt) +{ + exp->loop.mode = LottieExpression::LoopMode::OutCycle; + + if (argsCnt > 0) { + auto name = _name(args[0]); + if (!strcmp(name, EXP_CYCLE)) exp->loop.mode = LottieExpression::LoopMode::OutCycle; + else if (!strcmp(name, EXP_PINGPONG)) exp->loop.mode = LottieExpression::LoopMode::OutPingPong; + else if (!strcmp(name, EXP_OFFSET)) exp->loop.mode = LottieExpression::LoopMode::OutOffset; + else if (!strcmp(name, EXP_CONTINUE)) exp->loop.mode = LottieExpression::LoopMode::OutContinue; + free(name); + } + + if (exp->loop.mode != LottieExpression::LoopMode::OutCycle) { + TVGERR("hermet", "Not supported loopOut type = %d", exp->loop.mode); + return false; + } + + return true; +} + + +static jerry_value_t _loopOut(const jerry_call_info_t* info, const jerry_value_t args[], const jerry_length_t argsCnt) +{ + auto exp = static_cast(jerry_object_get_native_ptr(info->function, nullptr)); + + if (!_loopOutCommon(exp, args, argsCnt)) return jerry_undefined(); + + if (argsCnt > 1) exp->loop.key = jerry_value_as_int32(args[1]); + + auto obj = jerry_object(); + jerry_object_set_native_ptr(obj, nullptr, exp->property); + return obj; +} + + +static jerry_value_t _loopOutDuration(const jerry_call_info_t* info, const jerry_value_t args[], const jerry_length_t argsCnt) +{ + auto exp = static_cast(jerry_object_get_native_ptr(info->function, nullptr)); + + if (!_loopOutCommon(exp, args, argsCnt)) return jerry_undefined(); + + if (argsCnt > 1) { + exp->loop.in = exp->comp->frameAtTime((float)jerry_value_as_int32(args[1])); + } + + auto obj = jerry_object(); + jerry_object_set_native_ptr(obj, nullptr, exp->property); + return obj; +} + + +static bool _loopInCommon(LottieExpression* exp, const jerry_value_t args[], const jerry_length_t argsCnt) +{ + exp->loop.mode = LottieExpression::LoopMode::InCycle; + + if (argsCnt > 0) { + auto name = _name(args[0]); + if (!strcmp(name, EXP_CYCLE)) exp->loop.mode = LottieExpression::LoopMode::InCycle; + else if (!strcmp(name, EXP_PINGPONG)) exp->loop.mode = LottieExpression::LoopMode::InPingPong; + else if (!strcmp(name, EXP_OFFSET)) exp->loop.mode = LottieExpression::LoopMode::InOffset; + else if (!strcmp(name, EXP_CONTINUE)) exp->loop.mode = LottieExpression::LoopMode::InContinue; + free(name); + } + + if (exp->loop.mode != LottieExpression::LoopMode::InCycle) { + TVGERR("hermet", "Not supported loopOut type = %d", exp->loop.mode); + return false; + } + + return true; +} + +static jerry_value_t _loopIn(const jerry_call_info_t* info, const jerry_value_t args[], const jerry_length_t argsCnt) +{ + auto exp = static_cast(jerry_object_get_native_ptr(info->function, nullptr)); + + if (!_loopInCommon(exp, args, argsCnt)) return jerry_undefined(); + + if (argsCnt > 1) { + exp->loop.in = exp->comp->frameAtTime((float)jerry_value_as_int32(args[1])); + } + + auto obj = jerry_object(); + jerry_object_set_native_ptr(obj, nullptr, exp->property); + return obj; +} + + +static jerry_value_t _loopInDuration(const jerry_call_info_t* info, const jerry_value_t args[], const jerry_length_t argsCnt) +{ + auto exp = static_cast(jerry_object_get_native_ptr(info->function, nullptr)); + + if (argsCnt > 1) { + exp->loop.in = exp->comp->frameAtTime((float)jerry_value_as_int32(args[1])); + } + + if (!_loopInCommon(exp, args, argsCnt)) return jerry_undefined(); + + auto obj = jerry_object(); + jerry_object_set_native_ptr(obj, nullptr, exp->property); + return obj; +} + + +static jerry_value_t _key(const jerry_call_info_t* info, const jerry_value_t args[], const jerry_length_t argsCnt) +{ + auto exp = static_cast(jerry_object_get_native_ptr(info->function, nullptr)); + auto key = jerry_value_as_int32(args[0]); + auto frameNo = exp->property->frameNo(key); + auto time = jerry_number(exp->comp->timeAtFrame(frameNo)); + auto value = _value(frameNo, exp); + + auto obj = jerry_object(); + jerry_object_set_sz(obj, EXP_TIME, time); + jerry_object_set_sz(obj, EXP_INDEX, args[0]); + jerry_object_set_sz(obj, EXP_VALUE, value); + + jerry_value_free(time); + jerry_value_free(value); + + return obj; +} + + + +static jerry_value_t _createPath(const jerry_call_info_t* info, const jerry_value_t args[], const jerry_length_t argsCnt) +{ + //TODO: arg1: points, arg2: inTagents, arg3: outTangents, arg4: isClosed + auto arg1 = jerry_value_to_object(args[0]); + auto pathset = jerry_object_get_native_ptr(arg1, nullptr); + if (!pathset) { + TVGERR("LOTTIE", "failed createPath()"); + return jerry_undefined(); + } + + jerry_value_free(arg1); + + auto obj = jerry_object(); + jerry_object_set_native_ptr(obj, nullptr, pathset); + return obj; +} + + +static jerry_value_t _uniformPath(const jerry_call_info_t* info, const jerry_value_t args[], const jerry_length_t argsCnt) +{ + auto pathset = static_cast(jerry_object_get_native_ptr(info->function, nullptr)); + + /* TODO: ThorVG prebuilds the path data for performance. + It acutally need to constructs the Array for points, inTangents, outTangents and then return here... */ + auto obj = jerry_object(); + jerry_object_set_native_ptr(obj, nullptr, pathset); + return obj; +} + + +static jerry_value_t _isClosed(const jerry_call_info_t* info, const jerry_value_t args[], const jerry_length_t argsCnt) +{ + //TODO: Not used + return jerry_boolean(true); +} + + +static void _buildPath(jerry_value_t context, LottieExpression* exp) +{ + //Trick for fast buliding path. + auto points = jerry_function_external(_uniformPath); + jerry_object_set_native_ptr(points, nullptr, exp->property); + jerry_object_set_sz(context, "points", points); + jerry_value_free(points); + + auto inTangents = jerry_function_external(_uniformPath); + jerry_object_set_native_ptr(inTangents, nullptr, exp->property); + jerry_object_set_sz(context, "inTangents", inTangents); + jerry_value_free(inTangents); + + auto outTangents = jerry_function_external(_uniformPath); + jerry_object_set_native_ptr(outTangents, nullptr, exp->property); + jerry_object_set_sz(context, "outTangents", outTangents); + jerry_value_free(outTangents); + + auto isClosed = jerry_function_external(_isClosed); + jerry_object_set_native_ptr(isClosed, nullptr, exp->property); + jerry_object_set_sz(context, "isClosed", isClosed); + jerry_value_free(isClosed); + +} + + +static void _buildProperty(float frameNo, jerry_value_t context, LottieExpression* exp) +{ + auto value = _value(frameNo, exp); + jerry_object_set_sz(context, EXP_VALUE, value); + jerry_value_free(value); + + auto valueAtTime = jerry_function_external(_valueAtTime); + jerry_object_set_sz(context, "valueAtTime", valueAtTime); + jerry_object_set_native_ptr(valueAtTime, nullptr, exp); + jerry_value_free(valueAtTime); + + auto velocity = jerry_number(0.0f); + jerry_object_set_sz(context, "velocity", velocity); + jerry_value_free(velocity); + + auto velocityAtTime = jerry_function_external(_velocityAtTime); + jerry_object_set_sz(context, "velocityAtTime", velocityAtTime); + jerry_object_set_native_ptr(velocityAtTime, nullptr, exp); + jerry_value_free(velocityAtTime); + + auto speed = jerry_number(0.0f); + jerry_object_set_sz(context, "speed", speed); + jerry_value_free(speed); + + auto speedAtTime = jerry_function_external(_speedAtTime); + jerry_object_set_sz(context, "speedAtTime", speedAtTime); + jerry_object_set_native_ptr(speedAtTime, nullptr, exp); + jerry_value_free(speedAtTime); + + //wiggle(freq, amp, octaves=1, amp_mult=.5, t=time) + //temporalWiggle(freq, amp, octaves=1, amp_mult=.5, t=time) + //smooth(width=.2, samples=5, t=time) + + auto loopIn = jerry_function_external(_loopIn); + jerry_object_set_sz(context, "loopIn", loopIn); + jerry_object_set_native_ptr(loopIn, nullptr, exp); + jerry_value_free(loopIn); + + auto loopOut = jerry_function_external(_loopOut); + jerry_object_set_sz(context, "loopOut", loopOut); + jerry_object_set_native_ptr(loopOut, nullptr, exp); + jerry_value_free(loopOut); + + auto loopInDuration = jerry_function_external(_loopInDuration); + jerry_object_set_sz(context, "loopInDuration", loopInDuration); + jerry_object_set_native_ptr(loopInDuration, nullptr, exp); + jerry_value_free(loopInDuration); + + auto loopOutDuration = jerry_function_external(_loopOutDuration); + jerry_object_set_sz(context, "loopOutDuration", loopOutDuration); + jerry_object_set_native_ptr(loopOutDuration, nullptr, exp); + jerry_value_free(loopOutDuration); + + auto key = jerry_function_external(_key); + jerry_object_set_sz(context, "key", key); + jerry_object_set_native_ptr(key, nullptr, exp); + jerry_value_free(key); + + //key(markerName) + + auto nearestKey = jerry_function_external(_nearestKey); + jerry_object_set_native_ptr(nearestKey, nullptr, exp); + jerry_object_set_sz(context, "nearestKey", nearestKey); + jerry_value_free(nearestKey); + + auto numKeys = jerry_number(exp->property->frameCnt()); + jerry_object_set_sz(context, "numKeys", numKeys); + jerry_value_free(numKeys); + + //propertyGroup(countUp = 1) + //propertyIndex + //name + + //content("name"), #look for the named property from a layer + auto data = (ExpContent*)malloc(sizeof(ExpContent)); + data->obj = exp->layer; + data->frameNo = frameNo; + + auto content = jerry_function_external(_content); + jerry_object_set_sz(context, EXP_CONTENT, content); + jerry_object_set_native_ptr(content, &freeCb, data); + jerry_value_free(content); +} + + +static jerry_value_t _comp(const jerry_call_info_t* info, const jerry_value_t args[], const jerry_length_t argsCnt) +{ + auto comp = static_cast(jerry_object_get_native_ptr(info->function, nullptr)); + LottieLayer* layer; + + auto arg0 = jerry_value_to_string(args[0]); + auto len = jerry_string_length(arg0); + auto name = (jerry_char_t*)alloca(len * sizeof(jerry_char_t) + 1); + jerry_string_to_buffer(arg0, JERRY_ENCODING_UTF8, name, len); + name[len] = '\0'; + + jerry_value_free(arg0); + + layer = comp->asset((char*)name); + + if (!layer) return jerry_undefined(); + + auto obj = jerry_object(); + jerry_object_set_native_ptr(obj, nullptr, layer); + _buildLayer(obj, layer, comp); + + return obj; +} + + +static void _buildMath(jerry_value_t context) +{ + auto bm_mul = jerry_function_external(_mul); + jerry_object_set_sz(context, "$bm_mul", bm_mul); + jerry_value_free(bm_mul); + + auto bm_sum = jerry_function_external(_add); + jerry_object_set_sz(context, "$bm_sum", bm_sum); + jerry_value_free(bm_sum); + + auto bm_add = jerry_function_external(_add); + jerry_object_set_sz(context, "$bm_add", bm_add); + jerry_value_free(bm_add); + + auto bm_sub = jerry_function_external(_sub); + jerry_object_set_sz(context, "$bm_sub", bm_sub); + jerry_value_free(bm_sub); + + auto bm_div = jerry_function_external(_div); + jerry_object_set_sz(context, "$bm_div", bm_div); + jerry_value_free(bm_div); + + auto mul = jerry_function_external(_mul); + jerry_object_set_sz(context, "mul", mul); + jerry_value_free(mul); + + auto sum = jerry_function_external(_add); + jerry_object_set_sz(context, "sum", sum); + jerry_value_free(sum); + + auto add = jerry_function_external(_add); + jerry_object_set_sz(context, "add", add); + jerry_value_free(add); + + auto sub = jerry_function_external(_sub); + jerry_object_set_sz(context, "sub", sub); + jerry_value_free(sub); + + auto div = jerry_function_external(_div); + jerry_object_set_sz(context, "div", div); + jerry_value_free(div); + + auto clamp = jerry_function_external(_clamp); + jerry_object_set_sz(context, "clamp", clamp); + jerry_value_free(clamp); + + auto dot = jerry_function_external(_dot); + jerry_object_set_sz(context, "dot", dot); + jerry_value_free(dot); + + auto cross = jerry_function_external(_cross); + jerry_object_set_sz(context, "cross", cross); + jerry_value_free(cross); + + auto normalize = jerry_function_external(_normalize); + jerry_object_set_sz(context, "normalize", normalize); + jerry_value_free(normalize); + + auto length = jerry_function_external(_length); + jerry_object_set_sz(context, "length", length); + jerry_value_free(length); + + auto random = jerry_function_external(_random); + jerry_object_set_sz(context, "random", random); + jerry_value_free(random); + + auto deg2rad = jerry_function_external(_deg2rad); + jerry_object_set_sz(context, "degreesToRadians", deg2rad); + jerry_value_free(deg2rad); + + auto rad2deg = jerry_function_external(_rad2deg); + jerry_object_set_sz(context, "radiansToDegrees", rad2deg); + jerry_value_free(rad2deg); + + auto linear = jerry_function_external(_linear); + jerry_object_set_sz(context, "linear", linear); + jerry_value_free(linear); + + auto ease = jerry_function_external(_ease); + jerry_object_set_sz(context, "ease", ease); + jerry_value_free(ease); + + auto easeIn = jerry_function_external(_easeIn); + jerry_object_set_sz(context, "easeIn", easeIn); + jerry_value_free(easeIn); + + auto easeOut = jerry_function_external(_easeOut); + jerry_object_set_sz(context, "easeOut", easeOut); + jerry_value_free(easeOut); + + //lookAt +} + + +void LottieExpressions::buildComp(LottieComposition* comp) +{ + jerry_object_set_native_ptr(this->comp, nullptr, comp); + jerry_object_set_native_ptr(thisComp, nullptr, comp); + jerry_object_set_native_ptr(layer, nullptr, comp); + + //marker + //marker.key(index) + //marker.key(name) + //marker.nearestKey(t) + //marker.numKeys + + auto numLayers = jerry_number(comp->root->children.count); + jerry_object_set_sz(thisComp, "numLayers", numLayers); + jerry_value_free(numLayers); + + //activeCamera + + auto width = jerry_number(comp->w); + jerry_object_set_sz(thisComp, EXP_WIDTH, width); + jerry_value_free(width); + + auto height = jerry_number(comp->h); + jerry_object_set_sz(thisComp, EXP_HEIGHT, height); + jerry_value_free(height); + + auto duration = jerry_number(comp->duration()); + jerry_object_set_sz(thisComp, "duration", duration); + jerry_value_free(duration); + + //ntscDropFrame + //displayStartTime + + auto frameDuration = jerry_number(1.0f / comp->frameRate); + jerry_object_set_sz(thisComp, "frameDuration", frameDuration); + jerry_value_free(frameDuration); + + //shutterAngle + //shutterPhase + //bgColor + //pixelAspect + + auto name = jerry_string((jerry_char_t*)comp->name, strlen(comp->name), JERRY_ENCODING_UTF8); + jerry_object_set_sz(thisComp, EXP_NAME, name); + jerry_value_free(name); +} + + +jerry_value_t LottieExpressions::buildGlobal() +{ + global = jerry_current_realm(); + + //comp(name) + comp = jerry_function_external(_comp); + jerry_object_set_sz(global, "comp", comp); + + //footage(name) + + thisComp = jerry_object(); + jerry_object_set_sz(global, "thisComp", thisComp); + + //layer(index) / layer(name) / layer(otherLayer, reIndex) + layer = jerry_function_external(_layer); + jerry_object_set_sz(thisComp, "layer", layer); + + thisLayer = jerry_object(); + jerry_object_set_sz(global, "thisLayer", thisLayer); + + thisProperty = jerry_object(); + jerry_object_set_sz(global, "thisProperty", thisProperty); + + auto effect = jerry_function_external(_effect); + jerry_object_set_sz(global, EXP_EFFECT, effect); + jerry_value_free(effect); + + auto fromCompToSurface = jerry_function_external(_fromCompToSurface); + jerry_object_set_sz(global, "fromCompToSurface", fromCompToSurface); + jerry_value_free(fromCompToSurface); + + auto createPath = jerry_function_external(_createPath); + jerry_object_set_sz(global, "createPath", createPath); + jerry_value_free(createPath); + + //posterizeTime(framesPerSecond) + //value + + return global; +} + + +jerry_value_t LottieExpressions::evaluate(float frameNo, LottieExpression* exp) +{ + buildComp(exp->comp); + + //update global context values + jerry_object_set_native_ptr(thisLayer, nullptr, exp->layer); + _buildLayer(thisLayer, exp->layer, exp->comp); + + jerry_object_set_native_ptr(thisProperty, nullptr, exp->property); + _buildProperty(frameNo, global, exp); + + if (exp->type == LottieProperty::Type::PathSet) _buildPath(thisProperty, exp); + if (exp->object->type == LottieObject::Transform) _buildTransform(global, static_cast(exp->object)); + + //evaluate the code + auto eval = jerry_eval((jerry_char_t *) exp->code, strlen(exp->code), JERRY_PARSE_NO_OPTS); + + if (jerry_value_is_exception(eval) || jerry_value_is_undefined(eval)) { + exp->enabled = false; // The feature is experimental, it will be forcely turned off if it's incompatible. + return jerry_undefined(); + } + + jerry_value_free(eval); + + return jerry_object_get_sz(global, "$bm_rt"); +} + + +/************************************************************************/ +/* External Class Implementation */ +/************************************************************************/ + +LottieExpressions::~LottieExpressions() +{ + jerry_value_free(thisProperty); + jerry_value_free(thisLayer); + jerry_value_free(layer); + jerry_value_free(thisComp); + jerry_value_free(comp); + jerry_value_free(global); + jerry_cleanup(); +} + + +LottieExpressions::LottieExpressions() +{ + jerry_init(JERRY_INIT_EMPTY); + _buildMath(buildGlobal()); +} + + +void LottieExpressions::update(float curTime) +{ + //time, #current time in seconds + auto time = jerry_number(curTime); + jerry_object_set_sz(global, EXP_TIME, time); + jerry_value_free(time); +} + + +//FIXME: Threads support +#include "tvgTaskScheduler.h" + +LottieExpressions* LottieExpressions::instance() +{ + //FIXME: Threads support + if (TaskScheduler::threads() > 1) { + TVGLOG("LOTTIE", "Lottie Expressions are not supported with tvg threads"); + return nullptr; + } + + if (!exps) exps = new LottieExpressions; + ++engineRefCnt; + return exps; +} + + +void LottieExpressions::retrieve(LottieExpressions* instance) +{ + if (--engineRefCnt == 0) { + delete(instance); + exps = nullptr; + } +} + + +#endif //THORVG_LOTTIE_EXPRESSIONS_SUPPORT + +#endif /* LV_USE_THORVG_INTERNAL */ + diff --git a/src/libs/thorvg/tvgLottieExpressions.h b/src/libs/thorvg/tvgLottieExpressions.h new file mode 100644 index 000000000..14819a8fe --- /dev/null +++ b/src/libs/thorvg/tvgLottieExpressions.h @@ -0,0 +1,171 @@ +/* + * Copyright (c) 2024 the ThorVG project. All rights reserved. + + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#include "../../lv_conf_internal.h" +#if LV_USE_THORVG_INTERNAL + +#ifndef _TVG_LOTTIE_EXPRESSIONS_H_ +#define _TVG_LOTTIE_EXPRESSIONS_H_ + +#include "tvgCommon.h" + +struct LottieExpression; +struct LottieComposition; +struct RGB24; + +#ifdef THORVG_LOTTIE_EXPRESSIONS_SUPPORT + +#include "jerryscript.h" + + +struct LottieExpressions +{ +public: + template + bool result(float frameNo, NumType& out, LottieExpression* exp) + { + auto bm_rt = evaluate(frameNo, exp); + + if (auto prop = static_cast(jerry_object_get_native_ptr(bm_rt, nullptr))) { + out = (*prop)(frameNo); + } else if (jerry_value_is_number(bm_rt)) { + out = (NumType) jerry_value_as_number(bm_rt); + } else { + TVGERR("LOTTIE", "Failed dispatching a Value!"); + return false; + } + jerry_value_free(bm_rt); + return true; + } + + template + bool result(float frameNo, Point& out, LottieExpression* exp) + { + auto bm_rt = evaluate(frameNo, exp); + + if (jerry_value_is_object(bm_rt)) { + if (auto prop = static_cast(jerry_object_get_native_ptr(bm_rt, nullptr))) { + out = (*prop)(frameNo); + } else { + auto x = jerry_object_get_index(bm_rt, 0); + auto y = jerry_object_get_index(bm_rt, 1); + out.x = jerry_value_as_number(x); + out.y = jerry_value_as_number(y); + jerry_value_free(x); + jerry_value_free(y); + } + } else { + TVGERR("LOTTIE", "Failed dispatching Point!"); + return false; + } + jerry_value_free(bm_rt); + return true; + } + + template + bool result(float frameNo, RGB24& out, LottieExpression* exp) + { + auto bm_rt = evaluate(frameNo, exp); + + if (auto color = static_cast(jerry_object_get_native_ptr(bm_rt, nullptr))) { + out = (*color)(frameNo); + } else { + TVGERR("LOTTIE", "Failed dispatching Color!"); + return false; + } + jerry_value_free(bm_rt); + return true; + } + + template + bool result(float frameNo, Fill* fill, LottieExpression* exp) + { + auto bm_rt = evaluate(frameNo, exp); + + if (auto colorStop = static_cast(jerry_object_get_native_ptr(bm_rt, nullptr))) { + (*colorStop)(frameNo, fill, this); + } else { + TVGERR("LOTTIE", "Failed dispatching ColorStop!"); + return false; + } + jerry_value_free(bm_rt); + return true; + } + + template + bool result(float frameNo, Array& cmds, Array& pts, Matrix* transform, float roundness, LottieExpression* exp) + { + auto bm_rt = evaluate(frameNo, exp); + + if (auto pathset = static_cast(jerry_object_get_native_ptr(bm_rt, nullptr))) { + (*pathset)(frameNo, cmds, pts, transform, roundness); + } else { + TVGERR("LOTTIE", "Failed dispatching PathSet!"); + return false; + } + jerry_value_free(bm_rt); + return true; + } + + void update(float curTime); + + //singleton (no thread safety) + static LottieExpressions* instance(); + static void retrieve(LottieExpressions* instance); + +private: + LottieExpressions(); + ~LottieExpressions(); + + jerry_value_t evaluate(float frameNo, LottieExpression* exp); + jerry_value_t buildGlobal(); + void buildComp(LottieComposition* comp); + + //global object, attributes, methods + jerry_value_t global; + jerry_value_t comp; + jerry_value_t layer; + jerry_value_t thisComp; + jerry_value_t thisLayer; + jerry_value_t thisProperty; +}; + +#else + +struct LottieExpressions +{ + template bool result(TVG_UNUSED float, TVG_UNUSED NumType&, TVG_UNUSED LottieExpression*) { return false; } + template bool result(TVG_UNUSED float, TVG_UNUSED Point&, LottieExpression*) { return false; } + template bool result(TVG_UNUSED float, TVG_UNUSED RGB24&, TVG_UNUSED LottieExpression*) { return false; } + template bool result(TVG_UNUSED float, TVG_UNUSED Fill*, TVG_UNUSED LottieExpression*) { return false; } + template bool result(TVG_UNUSED float, TVG_UNUSED Array&, TVG_UNUSED Array&, TVG_UNUSED Matrix* transform, TVG_UNUSED float, TVG_UNUSED LottieExpression*) { return false; } + void update(TVG_UNUSED float) {} + static LottieExpressions* instance() { return nullptr; } + static void retrieve(TVG_UNUSED LottieExpressions* instance) {} +}; + +#endif //THORVG_LOTTIE_EXPRESSIONS_SUPPORT + +#endif //_TVG_LOTTIE_EXPRESSIONS_H_ + +#endif /* LV_USE_THORVG_INTERNAL */ + diff --git a/src/libs/thorvg/tvgLottieInterpolator.cpp b/src/libs/thorvg/tvgLottieInterpolator.cpp index 9db5c6515..c92bfb6e8 100644 --- a/src/libs/thorvg/tvgLottieInterpolator.cpp +++ b/src/libs/thorvg/tvgLottieInterpolator.cpp @@ -43,20 +43,20 @@ #define SUBDIVISION_MAX_ITERATIONS 10 -static inline float tvg_A(float aA1, float aA2) { return 1.0f - 3.0f * aA2 + 3.0f * aA1; } -static inline float tvg_B(float aA1, float aA2) { return 3.0f * aA2 - 6.0f * aA1; } -static inline float tvg_C(float aA1) { return 3.0f * aA1; } +static inline float _constA(float aA1, float aA2) { return 1.0f - 3.0f * aA2 + 3.0f * aA1; } +static inline float _constB(float aA1, float aA2) { return 3.0f * aA2 - 6.0f * aA1; } +static inline float _constC(float aA1) { return 3.0f * aA1; } static inline float _getSlope(float t, float aA1, float aA2) { - return 3.0f * tvg_A(aA1, aA2) * t * t + 2.0f * tvg_B(aA1, aA2) * t + tvg_C(aA1); + return 3.0f * _constA(aA1, aA2) * t * t + 2.0f * _constB(aA1, aA2) * t + _constC(aA1); } static inline float _calcBezier(float t, float aA1, float aA2) { - return ((tvg_A(aA1, aA2) * t + tvg_B(aA1, aA2)) * t + tvg_C(aA1)) * t; + return ((_constA(aA1, aA2) * t + _constB(aA1, aA2)) * t + _constC(aA1)) * t; } diff --git a/src/libs/thorvg/tvgLottieLoader.cpp b/src/libs/thorvg/tvgLottieLoader.cpp index 2a72e7cd7..cbc0a4d19 100644 --- a/src/libs/thorvg/tvgLottieLoader.cpp +++ b/src/libs/thorvg/tvgLottieLoader.cpp @@ -33,23 +33,6 @@ /* Internal Class Implementation */ /************************************************************************/ -static bool _checkDotLottie(const char *str) -{ - //check the .Lottie signature. - if (str[0] == 0x50 && str[1] == 0x4B && str[2] == 0x03 && str[3] == 0x04) return true; - else return false; -} - - -static float _str2float(const char* str, int len) -{ - auto tmp = strDuplicate(str, len); - auto ret = strToFloat(tmp, nullptr); - free(tmp); - return ret; -} - - void LottieLoader::run(unsigned tid) { //update frame @@ -62,6 +45,7 @@ void LottieLoader::run(unsigned tid) comp = parser.comp; builder->build(comp); } + rebuild = false; } @@ -138,7 +122,7 @@ bool LottieLoader::header() p += 5; auto e = strstr(p, ","); if (!e) e = strstr(p, "}"); - frameRate = _str2float(p, e - p); + frameRate = strToFloat(p, nullptr); p = e; continue; } @@ -148,7 +132,7 @@ bool LottieLoader::header() p += 5; auto e = strstr(p, ","); if (!e) e = strstr(p, "}"); - startFrame = _str2float(p, e - p); + startFrame = strToFloat(p, nullptr); p = e; continue; } @@ -158,7 +142,7 @@ bool LottieLoader::header() p += 5; auto e = strstr(p, ","); if (!e) e = strstr(p, "}"); - endFrame = _str2float(p, e - p); + endFrame = strToFloat(p, nullptr); p = e; continue; } @@ -168,7 +152,7 @@ bool LottieLoader::header() p += 4; auto e = strstr(p, ","); if (!e) e = strstr(p, "}"); - w = static_cast(str2int(p, e - p)); + w = strToFloat(p, nullptr); p = e; continue; } @@ -177,14 +161,14 @@ bool LottieLoader::header() p += 4; auto e = strstr(p, ","); if (!e) e = strstr(p, "}"); - h = static_cast(str2int(p, e - p)); + h = strToFloat(p, nullptr); p = e; continue; } ++p; } - if (frameRate < FLT_EPSILON) { + if (frameRate < FLOAT_EPSILON) { TVGLOG("LOTTIE", "Not a Lottie file? Frame rate is 0!"); return false; } @@ -192,7 +176,7 @@ bool LottieLoader::header() frameCnt = (endFrame - startFrame); frameDuration = frameCnt / frameRate; - TVGLOG("LOTTIE", "info: frame rate = %f, duration = %f size = %d x %d", frameRate, frameDuration, (int)w, (int)h); + TVGLOG("LOTTIE", "info: frame rate = %f, duration = %f size = %f x %f", frameRate, frameDuration, w, h); return true; } @@ -200,13 +184,6 @@ bool LottieLoader::header() bool LottieLoader::open(const char* data, uint32_t size, bool copy) { - //If the format is dotLottie - auto dotLottie = _checkDotLottie(data); - if (dotLottie) { - TVGLOG("LOTTIE", "Requested .Lottie Format, Not Supported yet."); - return false; - } - if (copy) { content = (char*)malloc(size); if (!content) return false; @@ -244,13 +221,6 @@ bool LottieLoader::open(const string& path) fclose(f); - //If the format is dotLottie - auto dotLottie = _checkDotLottie(content); - if (dotLottie) { - TVGLOG("LOTTIE", "Requested .Lottie Format, Not Supported yet."); - return false; - } - this->dirName = strDirname(path.c_str()); this->content = content; this->copy = true; @@ -292,7 +262,8 @@ bool LottieLoader::read() Paint* LottieLoader::paint() { - this->done(); + done(); + if (!comp) return nullptr; comp->initiated = true; return comp->root->scene; @@ -301,39 +272,60 @@ Paint* LottieLoader::paint() bool LottieLoader::override(const char* slot) { - if (!slot || !comp || comp->slots.count == 0) return false; + if (!comp) done(); - //TODO: Crashed, does this necessary? - auto temp = strdup(slot); + if (!comp || comp->slots.count == 0) return false; - //parsing slot json - LottieParser parser(temp, dirName); - auto sid = parser.sid(); - if (!sid) { + auto success = true; + + //override slots + if (slot) { + //Copy the input data because the JSON parser will encode the data immediately. + auto temp = strdup(slot); + + //parsing slot json + LottieParser parser(temp, dirName); + + auto idx = 0; + while (auto sid = parser.sid(idx == 0)) { + for (auto s = comp->slots.begin(); s < comp->slots.end(); ++s) { + if (strcmp((*s)->sid, sid)) continue; + if (!parser.apply(*s)) success = false; + break; + } + ++idx; + } + + if (idx < 1) success = false; free(temp); - return false; + rebuild = overriden = success; + //reset slots + } else if (overriden) { + for (auto s = comp->slots.begin(); s < comp->slots.end(); ++s) { + (*s)->reset(); + } + overriden = false; + rebuild = true; } - - bool ret = false; - for (auto s = comp->slots.begin(); s < comp->slots.end(); ++s) { - if (strcmp((*s)->sid, sid)) continue; - ret = parser.parse(*s); - break; - } - - free(temp); - return ret; + return success; } bool LottieLoader::frame(float no) { - //no meaing to update if frame diff is less then 1ms - if (fabsf(this->frameNo - no) < 0.001f) return false; + auto frameNo = no + startFrame(); + + //This ensures that the target frame number is reached. + frameNo *= 10000.0f; + frameNo = roundf(frameNo); + frameNo *= 0.0001f; + + //Skip update if frame diff is too small. + if (fabsf(this->frameNo - frameNo) <= 0.0009f) return false; this->done(); - this->frameNo = no; + this->frameNo = frameNo; TaskScheduler::request(this); @@ -341,28 +333,75 @@ bool LottieLoader::frame(float no) } +float LottieLoader::startFrame() +{ + return frameCnt * segmentBegin; +} + + float LottieLoader::totalFrame() { - return frameCnt; + return (segmentEnd - segmentBegin) * frameCnt; } float LottieLoader::curFrame() { - return frameNo; + return frameNo - startFrame(); } float LottieLoader::duration() { - return frameDuration; + if (segmentBegin == 0.0f && segmentEnd == 1.0f) return frameDuration; + + if (!comp) done(); + if (!comp) return 0.0f; + + return frameCnt * (segmentEnd - segmentBegin) / comp->frameRate; } void LottieLoader::sync() { this->done(); + + if (rebuild) run(0); } + +uint32_t LottieLoader::markersCnt() +{ + if (!comp) done(); + if (!comp) return 0; + return comp->markers.count; +} + + +const char* LottieLoader::markers(uint32_t index) +{ + if (!comp) done(); + if (!comp || index >= comp->markers.count) return nullptr; + auto marker = comp->markers.begin() + index; + return (*marker)->name; +} + + +bool LottieLoader::segment(const char* marker, float& begin, float& end) +{ + if (!comp) done(); + if (!comp) return false; + + for (auto m = comp->markers.begin(); m < comp->markers.end(); ++m) { + if (!strcmp(marker, (*m)->name)) { + begin = (*m)->time / frameCnt; + end = ((*m)->time + (*m)->duration) / frameCnt; + return true; + } + } + return false; +} + + #endif /* LV_USE_THORVG_INTERNAL */ diff --git a/src/libs/thorvg/tvgLottieLoader.h b/src/libs/thorvg/tvgLottieLoader.h index 93c1a0ae2..38b4d3ac5 100644 --- a/src/libs/thorvg/tvgLottieLoader.h +++ b/src/libs/thorvg/tvgLottieLoader.h @@ -47,6 +47,8 @@ public: char* dirName = nullptr; //base resource directory bool copy = false; //"content" is owned by this loader + bool overriden = false; //overridden properties with slots + bool rebuild = false; //require building the lottie scene LottieLoader(); ~LottieLoader(); @@ -65,9 +67,15 @@ public: float duration() override; void sync() override; + //Marker Supports + uint32_t markersCnt(); + const char* markers(uint32_t index); + bool segment(const char* marker, float& beign, float& end); + private: bool header(); void clear(); + float startFrame(); void run(unsigned tid) override; }; diff --git a/src/libs/thorvg/tvgLottieModel.cpp b/src/libs/thorvg/tvgLottieModel.cpp index a21896b0f..226c24cab 100644 --- a/src/libs/thorvg/tvgLottieModel.cpp +++ b/src/libs/thorvg/tvgLottieModel.cpp @@ -38,6 +38,79 @@ /* External Class Implementation */ /************************************************************************/ +void LottieSlot::reset() +{ + if (!overriden) return; + + for (auto pair = pairs.begin(); pair < pairs.end(); ++pair) { + switch (type) { + case LottieProperty::Type::ColorStop: { + static_cast(pair->obj)->colorStops.release(); + static_cast(pair->obj)->colorStops = *static_cast(pair->prop); + static_cast(pair->prop)->frames = nullptr; + break; + } + case LottieProperty::Type::Color: { + static_cast(pair->obj)->color.release(); + static_cast(pair->obj)->color = *static_cast(pair->prop); + static_cast(pair->prop)->frames = nullptr; + break; + } + case LottieProperty::Type::TextDoc: { + static_cast(pair->obj)->doc.release(); + static_cast(pair->obj)->doc = *static_cast(pair->prop); + static_cast(pair->prop)->frames = nullptr; + break; + } + default: break; + } + delete(pair->prop); + pair->prop = nullptr; + } + overriden = false; +} + + +void LottieSlot::assign(LottieObject* target) +{ + //apply slot object to all targets + for (auto pair = pairs.begin(); pair < pairs.end(); ++pair) { + //backup the original properties before overwriting + switch (type) { + case LottieProperty::Type::ColorStop: { + if (!overriden) { + pair->prop = new LottieColorStop; + *static_cast(pair->prop) = static_cast(pair->obj)->colorStops; + } + + pair->obj->override(&static_cast(target)->colorStops); + break; + } + case LottieProperty::Type::Color: { + if (!overriden) { + pair->prop = new LottieColor; + *static_cast(pair->prop) = static_cast(pair->obj)->color; + } + + pair->obj->override(&static_cast(target)->color); + break; + } + case LottieProperty::Type::TextDoc: { + if (!overriden) { + pair->prop = new LottieTextDoc; + *static_cast(pair->prop) = static_cast(pair->obj)->doc; + } + + pair->obj->override(&static_cast(target)->doc); + break; + } + default: break; + } + } + overriden = true; +} + + LottieImage::~LottieImage() { free(b64Data); @@ -49,11 +122,11 @@ LottieImage::~LottieImage() } -void LottieTrimpath::segment(float frameNo, float& start, float& end) +void LottieTrimpath::segment(float frameNo, float& start, float& end, LottieExpressions* exps) { - auto s = this->start(frameNo) * 0.01f; - auto e = this->end(frameNo) * 0.01f; - auto o = fmod(this->offset(frameNo), 360.0f) / 360.0f; //0 ~ 1 + auto s = this->start(frameNo, exps) * 0.01f; + auto e = this->end(frameNo, exps) * 0.01f; + auto o = fmodf(this->offset(frameNo, exps), 360.0f) / 360.0f; //0 ~ 1 auto diff = fabs(s - e); if (mathZero(diff)) { @@ -92,44 +165,126 @@ void LottieTrimpath::segment(float frameNo, float& start, float& end) } -Fill* LottieGradient::fill(float frameNo) +uint32_t LottieGradient::populate(ColorStop& color) +{ + colorStops.populated = true; + if (!color.input) return 0; + + uint32_t alphaCnt = (color.input->count - (colorStops.count * 4)) / 2; + Array output(colorStops.count + alphaCnt); + uint32_t cidx = 0; //color count + uint32_t clast = colorStops.count * 4; + if (clast > color.input->count) clast = color.input->count; + uint32_t aidx = clast; //alpha count + Fill::ColorStop cs; + + //merge color stops. + for (uint32_t i = 0; i < color.input->count; ++i) { + if (cidx == clast || aidx == color.input->count) break; + if ((*color.input)[cidx] == (*color.input)[aidx]) { + cs.offset = (*color.input)[cidx]; + cs.r = lroundf((*color.input)[cidx + 1] * 255.0f); + cs.g = lroundf((*color.input)[cidx + 2] * 255.0f); + cs.b = lroundf((*color.input)[cidx + 3] * 255.0f); + cs.a = lroundf((*color.input)[aidx + 1] * 255.0f); + cidx += 4; + aidx += 2; + } else if ((*color.input)[cidx] < (*color.input)[aidx]) { + cs.offset = (*color.input)[cidx]; + cs.r = lroundf((*color.input)[cidx + 1] * 255.0f); + cs.g = lroundf((*color.input)[cidx + 2] * 255.0f); + cs.b = lroundf((*color.input)[cidx + 3] * 255.0f); + //generate alpha value + if (output.count > 0) { + auto p = ((*color.input)[cidx] - output.last().offset) / ((*color.input)[aidx] - output.last().offset); + cs.a = mathLerp(output.last().a, lroundf((*color.input)[aidx + 1] * 255.0f), p); + } else cs.a = 255; + cidx += 4; + } else { + cs.offset = (*color.input)[aidx]; + cs.a = lroundf((*color.input)[aidx + 1] * 255.0f); + //generate color value + if (output.count > 0) { + auto p = ((*color.input)[aidx] - output.last().offset) / ((*color.input)[cidx] - output.last().offset); + cs.r = mathLerp(output.last().r, lroundf((*color.input)[cidx + 1] * 255.0f), p); + cs.g = mathLerp(output.last().g, lroundf((*color.input)[cidx + 2] * 255.0f), p); + cs.b = mathLerp(output.last().b, lroundf((*color.input)[cidx + 3] * 255.0f), p); + } else cs.r = cs.g = cs.b = 255; + aidx += 2; + } + output.push(cs); + } + + //color remains + while (cidx + 3 < clast) { + cs.offset = (*color.input)[cidx]; + cs.r = lroundf((*color.input)[cidx + 1] * 255.0f); + cs.g = lroundf((*color.input)[cidx + 2] * 255.0f); + cs.b = lroundf((*color.input)[cidx + 3] * 255.0f); + cs.a = (output.count > 0) ? output.last().a : 255; + output.push(cs); + cidx += 4; + } + + //alpha remains + while (aidx < color.input->count) { + cs.offset = (*color.input)[aidx]; + cs.a = lroundf((*color.input)[aidx + 1] * 255.0f); + if (output.count > 0) { + cs.r = output.last().r; + cs.g = output.last().g; + cs.b = output.last().b; + } else cs.r = cs.g = cs.b = 255; + output.push(cs); + aidx += 2; + } + + color.data = output.data; + output.data = nullptr; + + color.input->reset(); + delete(color.input); + + return output.count; +} + + +Fill* LottieGradient::fill(float frameNo, LottieExpressions* exps) { Fill* fill = nullptr; + auto s = start(frameNo, exps); + auto e = end(frameNo, exps); //Linear Graident if (id == 1) { fill = LinearGradient::gen().release(); - static_cast(fill)->linear(start(frameNo).x, start(frameNo).y, end(frameNo).x, end(frameNo).y); + static_cast(fill)->linear(s.x, s.y, e.x, e.y); } //Radial Gradient if (id == 2) { fill = RadialGradient::gen().release(); - auto sx = start(frameNo).x; - auto sy = start(frameNo).y; - auto ex = end(frameNo).x; - auto ey = end(frameNo).y; - auto w = fabsf(ex - sx); - auto h = fabsf(ey - sy); + auto w = fabsf(e.x - s.x); + auto h = fabsf(e.y - s.y); auto r = (w > h) ? (w + 0.375f * h) : (h + 0.375f * w); - auto progress = this->height(frameNo) * 0.01f; + auto progress = this->height(frameNo, exps) * 0.01f; if (mathZero(progress)) { - P(static_cast(fill))->radial(sx, sy, r, sx, sy, 0.0f); + P(static_cast(fill))->radial(s.x, s.y, r, s.x, s.y, 0.0f); } else { if (mathEqual(progress, 1.0f)) progress = 0.99f; - auto startAngle = atan2(ey - sy, ex - sx) * 180.0f / MATH_PI; - auto angle = (startAngle + this->angle(frameNo)) * (MATH_PI / 180.0f); - auto fx = sx + cos(angle) * progress * r; - auto fy = sy + sin(angle) * progress * r; + auto startAngle = mathRad2Deg(atan2(e.y - s.y, e.x - s.x)); + auto angle = mathDeg2Rad((startAngle + this->angle(frameNo, exps))); + auto fx = s.x + cos(angle) * progress * r; + auto fy = s.y + sin(angle) * progress * r; // Lottie dosen't have any focal radius concept - P(static_cast(fill))->radial(sx, sy, r, fx, fy, 0.0f); + P(static_cast(fill))->radial(s.x, s.y, r, fx, fy, 0.0f); } } if (!fill) return nullptr; - colorStops(frameNo, fill); + colorStops(frameNo, fill, exps); return fill; } @@ -145,14 +300,20 @@ void LottieGroup::prepare(LottieObject::Type type) size_t fillCnt = 0; for (auto c = children.end() - 1; c >= children.begin(); --c) { - if (reqFragment && !statical) break; auto child = static_cast(*c); - if (statical) statical &= child->statical; + + if (child->type == LottieObject::Type::Trimpath) trimpath = true; + + /* Figure out if this group is a simple path drawing. + In that case, the rendering context can be sharable with the parent's. */ + if (allowMerge && (child->type == LottieObject::Group || !child->mergeable())) allowMerge = false; + + if (reqFragment) continue; + /* Figure out if the rendering context should be fragmented. Multiple stroking or grouping with a stroking would occur this. This fragment resolves the overlapped stroke outlines. */ - if (reqFragment) continue; - if (child->type == LottieObject::Group) { + if (child->type == LottieObject::Group && !child->mergeable()) { if (strokeCnt > 0 || fillCnt > 0) reqFragment = true; } else if (child->type == LottieObject::SolidStroke || child->type == LottieObject::GradientStroke) { if (strokeCnt > 0) reqFragment = true; @@ -162,6 +323,25 @@ void LottieGroup::prepare(LottieObject::Type type) else ++fillCnt; } } + + //Reverse the drawing order if this group has a trimpath. + if (!trimpath) return; + + for (uint32_t i = 0; i < children.count - 1; ) { + auto child2 = children[i + 1]; + if (!child2->mergeable() || child2->type == LottieObject::Transform) { + i += 2; + continue; + } + auto child = children[i]; + if (!child->mergeable() || child->type == LottieObject::Transform) { + i++; + continue; + } + children[i] = child2; + children[i + 1] = child; + i++; + } } @@ -183,13 +363,11 @@ LottieLayer::~LottieLayer() void LottieLayer::prepare() { - if (transform) statical &= transform->statical; - if (timeRemap.frames) statical = false; - /* if layer is hidden, only useful data is its transform matrix. - so force it to be a Null Layer and release all resource. */ + so force it to be a Null Layer and release all resource. */ if (hidden) { type = LottieLayer::Null; + for (auto p = children.begin(); p < children.end(); ++p) delete(*p); children.reset(); return; } @@ -197,10 +375,10 @@ void LottieLayer::prepare() } -float LottieLayer::remap(float frameNo) +float LottieLayer::remap(float frameNo, LottieExpressions* exp) { if (timeRemap.frames || timeRemap.value) { - frameNo = comp->frameAtTime(timeRemap(frameNo)); + frameNo = comp->frameAtTime(timeRemap(frameNo, exp)); } else { frameNo -= startFrame; } @@ -210,7 +388,7 @@ float LottieLayer::remap(float frameNo) LottieComposition::~LottieComposition() { - if (!initiated) delete(root->scene); + if (!initiated && root) delete(root->scene); delete(root); free(version); @@ -236,6 +414,10 @@ LottieComposition::~LottieComposition() for (auto s = slots.begin(); s < slots.end(); ++s) { delete(*s); } + + for (auto m = markers.begin(); m < markers.end(); ++m) { + delete(*m); + } } #endif /* LV_USE_THORVG_INTERNAL */ diff --git a/src/libs/thorvg/tvgLottieModel.h b/src/libs/thorvg/tvgLottieModel.h index a650518e6..277e3d264 100644 --- a/src/libs/thorvg/tvgLottieModel.h +++ b/src/libs/thorvg/tvgLottieModel.h @@ -54,29 +54,23 @@ struct LottieStroke return dashattr->value[no]; } - float dashOffset(float frameNo) + float dashOffset(float frameNo, LottieExpressions* exps) { - return dash(0)(frameNo); + return dash(0)(frameNo, exps); } - float dashGap(float frameNo) + float dashGap(float frameNo, LottieExpressions* exps) { - return dash(2)(frameNo); + return dash(2)(frameNo, exps); } - float dashSize(float frameNo) + float dashSize(float frameNo, LottieExpressions* exps) { - auto d = dash(1)(frameNo); + auto d = dash(1)(frameNo, exps); if (d == 0.0f) return 0.1f; else return d; } - bool dynamic() - { - if (width.frames || dashattr) return true; - return false; - } - LottieFloat width = 0.0f; DashAttr* dashattr = nullptr; float miterLimit = 0; @@ -91,12 +85,6 @@ struct LottieMask LottieOpacity opacity = 255; CompositeMethod method; bool inverse = false; - - bool dynamic() - { - if (opacity.frames || pathset.frames) return true; - return false; - } }; @@ -128,14 +116,15 @@ struct LottieObject free(name); } - virtual void override(LottieObject* prop) + virtual void override(LottieProperty* prop) { TVGERR("LOTTIE", "Unsupported slot type"); } + virtual bool mergeable() { return false; } + char* name = nullptr; Type type; - bool statical = true; //no keyframes bool hidden = false; //remove? }; @@ -183,6 +172,17 @@ struct LottieFont Origin origin = Embedded; }; +struct LottieMarker +{ + char* name = nullptr; + float time = 0.0f; + float duration = 0.0f; + + ~LottieMarker() + { + free(name); + } +}; struct LottieText : LottieObject { @@ -191,9 +191,9 @@ struct LottieText : LottieObject LottieObject::type = LottieObject::Text; } - void override(LottieObject* prop) override + void override(LottieProperty* prop) override { - this->doc = static_cast(prop)->doc; + this->doc = *static_cast(prop); this->prepare(); } @@ -205,18 +205,23 @@ struct LottieText : LottieObject struct LottieTrimpath : LottieObject { - enum Type : uint8_t { Individual = 1, Simultaneous = 2 }; + enum Type : uint8_t { Simultaneous = 1, Individual = 2 }; void prepare() { LottieObject::type = LottieObject::Trimpath; - if (start.frames || end.frames || offset.frames) statical = false; } - void segment(float frameNo, float& start, float& end); + bool mergeable() override + { + if (!start.frames && start.value == 0.0f && !end.frames && end.value == 100.0f && !offset.frames && offset.value == 0.0f) return true; + return false; + } + + void segment(float frameNo, float& start, float& end, LottieExpressions* exps); LottieFloat start = 0.0f; - LottieFloat end = 0.0f; + LottieFloat end = 100.0f; LottieFloat offset = 0.0f; Type type = Simultaneous; }; @@ -226,6 +231,11 @@ struct LottieShape : LottieObject { virtual ~LottieShape() {} uint8_t direction = 0; //0: clockwise, 2: counter-clockwise, 3: xor(?) + + bool mergeable() override + { + return true; + } }; @@ -234,7 +244,6 @@ struct LottieRoundedCorner : LottieObject void prepare() { LottieObject::type = LottieObject::RoundedCorner; - if (radius.frames) statical = false; } LottieFloat radius = 0.0f; }; @@ -245,7 +254,6 @@ struct LottiePath : LottieShape void prepare() { LottieObject::type = LottieObject::Path; - if (pathset.frames) statical = false; } LottiePathSet pathset; @@ -257,7 +265,6 @@ struct LottieRect : LottieShape void prepare() { LottieObject::type = LottieObject::Rect; - if (position.frames || size.frames || radius.frames) statical = false; } LottiePosition position = Point{0.0f, 0.0f}; @@ -273,7 +280,6 @@ struct LottiePolyStar : LottieShape void prepare() { LottieObject::type = LottieObject::Polystar; - if (position.frames || innerRadius.frames || outerRadius.frames || innerRoundness.frames || outerRoundness.frames || rotation.frames || ptsCnt.frames) statical = false; } LottiePosition position = Point{0.0f, 0.0f}; @@ -292,7 +298,6 @@ struct LottieEllipse : LottieShape void prepare() { LottieObject::type = LottieObject::Ellipse; - if (position.frames || size.frames) statical = false; } LottiePosition position = Point{0.0f, 0.0f}; @@ -323,9 +328,12 @@ struct LottieTransform : LottieObject void prepare() { LottieObject::type = LottieObject::Transform; - if (position.frames || rotation.frames || scale.frames || anchor.frames || opacity.frames || (coords && (coords->x.frames || coords->y.frames)) || (rotationEx && (rotationEx->x.frames || rotationEx->y.frames))) { - statical = false; - } + } + + bool mergeable() override + { + if (!opacity.frames && opacity.value == 255) return true; + return false; } LottiePosition position = Point{0.0f, 0.0f}; @@ -333,13 +341,15 @@ struct LottieTransform : LottieObject LottiePoint scale = Point{100.0f, 100.0f}; LottiePoint anchor = Point{0.0f, 0.0f}; LottieOpacity opacity = 255; + LottieFloat skewAngle = 0.0f; + LottieFloat skewAxis = 0.0f; SeparateCoord* coords = nullptr; //either a position or separate coordinates RotationEx* rotationEx = nullptr; //extension for 3d rotation }; -struct LottieSolid : LottieObject +struct LottieSolid : LottieObject { LottieColor color = RGB24{255, 255, 255}; LottieOpacity opacity = 255; @@ -351,12 +361,11 @@ struct LottieSolidStroke : LottieSolid, LottieStroke void prepare() { LottieObject::type = LottieObject::SolidStroke; - if (color.frames || opacity.frames || LottieStroke::dynamic()) statical = false; } - void override(LottieObject* prop) override + void override(LottieProperty* prop) override { - this->color = static_cast(prop)->color; + this->color = *static_cast(prop); this->prepare(); } }; @@ -367,12 +376,11 @@ struct LottieSolidFill : LottieSolid void prepare() { LottieObject::type = LottieObject::SolidFill; - if (color.frames || opacity.frames) statical = false; } - void override(LottieObject* prop) override + void override(LottieProperty* prop) override { - this->color = static_cast(prop)->color; + this->color = *static_cast(prop); this->prepare(); } @@ -382,99 +390,23 @@ struct LottieSolidFill : LottieSolid struct LottieGradient : LottieObject { - uint32_t populate(ColorStop& color) - { - uint32_t alphaCnt = (color.input->count - (colorStops.count * 4)) / 2; - Array output(colorStops.count + alphaCnt); - uint32_t cidx = 0; //color count - uint32_t clast = colorStops.count * 4; - uint32_t aidx = clast; //alpha count - Fill::ColorStop cs; - - //merge color stops. - for (uint32_t i = 0; i < color.input->count; ++i) { - if (cidx == clast || aidx == color.input->count) break; - if ((*color.input)[cidx] == (*color.input)[aidx]) { - cs.offset = (*color.input)[cidx]; - cs.r = lroundf((*color.input)[cidx + 1] * 255.0f); - cs.g = lroundf((*color.input)[cidx + 2] * 255.0f); - cs.b = lroundf((*color.input)[cidx + 3] * 255.0f); - cs.a = lroundf((*color.input)[aidx + 1] * 255.0f); - cidx += 4; - aidx += 2; - } else if ((*color.input)[cidx] < (*color.input)[aidx]) { - cs.offset = (*color.input)[cidx]; - cs.r = lroundf((*color.input)[cidx + 1] * 255.0f); - cs.g = lroundf((*color.input)[cidx + 2] * 255.0f); - cs.b = lroundf((*color.input)[cidx + 3] * 255.0f); - //generate alpha value - if (output.count > 0) { - auto p = ((*color.input)[cidx] - output.last().offset) / ((*color.input)[aidx] - output.last().offset); - cs.a = mathLerp(output.last().a, lroundf((*color.input)[aidx + 1] * 255.0f), p); - } else cs.a = 255; - cidx += 4; - } else { - cs.offset = (*color.input)[aidx]; - cs.a = lroundf((*color.input)[aidx + 1] * 255.0f); - //generate color value - if (output.count > 0) { - auto p = ((*color.input)[aidx] - output.last().offset) / ((*color.input)[cidx] - output.last().offset); - cs.r = mathLerp(output.last().r, lroundf((*color.input)[cidx + 1] * 255.0f), p); - cs.g = mathLerp(output.last().g, lroundf((*color.input)[cidx + 2] * 255.0f), p); - cs.b = mathLerp(output.last().b, lroundf((*color.input)[cidx + 3] * 255.0f), p); - } else cs.r = cs.g = cs.b = 255; - aidx += 2; - } - output.push(cs); - } - - //color remains - while (cidx < clast) { - cs.offset = (*color.input)[cidx]; - cs.r = lroundf((*color.input)[cidx + 1] * 255.0f); - cs.g = lroundf((*color.input)[cidx + 2] * 255.0f); - cs.b = lroundf((*color.input)[cidx + 3] * 255.0f); - cs.a = (output.count > 0) ? output.last().a : 255; - output.push(cs); - cidx += 4; - } - - //alpha remains - while (aidx < color.input->count) { - cs.offset = (*color.input)[aidx]; - cs.a = lroundf((*color.input)[aidx + 1] * 255.0f); - if (output.count > 0) { - cs.r = output.last().r; - cs.g = output.last().g; - cs.b = output.last().b; - } else cs.r = cs.g = cs.b = 255; - output.push(cs); - aidx += 2; - } - - color.data = output.data; - output.data = nullptr; - - color.input->reset(); - delete(color.input); - - return output.count; - } - bool prepare() { - if (colorStops.frames) { - for (auto v = colorStops.frames->begin(); v < colorStops.frames->end(); ++v) { - colorStops.count = populate(v->value); + if (!colorStops.populated) { + if (colorStops.frames) { + for (auto v = colorStops.frames->begin(); v < colorStops.frames->end(); ++v) { + colorStops.count = populate(v->value); + } + } else { + colorStops.count = populate(colorStops.value); } - } else { - colorStops.count = populate(colorStops.value); } if (start.frames || end.frames || height.frames || angle.frames || opacity.frames || colorStops.frames) return true; return false; } - Fill* fill(float frameNo); + uint32_t populate(ColorStop& color); + Fill* fill(float frameNo, LottieExpressions* exps); LottiePoint start = Point{0.0f, 0.0f}; LottiePoint end = Point{0.0f, 0.0f}; @@ -491,12 +423,12 @@ struct LottieGradientFill : LottieGradient void prepare() { LottieObject::type = LottieObject::GradientFill; - if (LottieGradient::prepare()) statical = false; + LottieGradient::prepare(); } - void override(LottieObject* prop) override + void override(LottieProperty* prop) override { - this->colorStops = static_cast(prop)->colorStops; + this->colorStops = *static_cast(prop); this->prepare(); } @@ -509,12 +441,12 @@ struct LottieGradientStroke : LottieGradient, LottieStroke void prepare() { LottieObject::type = LottieObject::GradientStroke; - if (LottieGradient::prepare() || LottieStroke::dynamic()) statical = false; + LottieGradient::prepare(); } - void override(LottieObject* prop) override + void override(LottieProperty* prop) override { - this->colorStops = static_cast(prop)->colorStops; + this->colorStops = *static_cast(prop); this->prepare(); } }; @@ -545,7 +477,6 @@ struct LottieRepeater : LottieObject void prepare() { LottieObject::type = LottieObject::Repeater; - if (copies.frames || offset.frames || position.frames || rotation.frames || scale.frames || anchor.frames || startOpacity.frames || endOpacity.frames) statical = false; } LottieFloat copies = 0.0f; @@ -570,12 +501,29 @@ struct LottieGroup : LottieObject } void prepare(LottieObject::Type type = LottieObject::Group); + bool mergeable() override { return allowMerge; } + + LottieObject* content(const char* id) + { + if (name && !strcmp(name, id)) return this; + + //source has children, find recursively. + for (auto c = children.begin(); c < children.end(); ++c) { + auto child = *c; + if (child->type == LottieObject::Type::Group || child->type == LottieObject::Type::Layer) { + if (auto ret = static_cast(child)->content(id)) return ret; + } else if (child->name && !strcmp(child->name, id)) return child; + } + return nullptr; + } Scene* scene = nullptr; //tvg render data Array children; bool reqFragment = false; //requirment to fragment the render context bool buildDone = false; //completed in building the composition. + bool allowMerge = true; //if this group is consisted of simple (transformed) shapes. + bool trimpath = false; //this group has a trimpath. }; @@ -592,8 +540,10 @@ struct LottieLayer : LottieGroup return transform->opacity(frameNo); } + bool mergeable() override { return false; } + void prepare(); - float remap(float frameNo); + float remap(float frameNo, LottieExpressions* exp); struct { CompositeMethod type = CompositeMethod::None; @@ -609,7 +559,7 @@ struct LottieLayer : LottieGroup RGB24 color; //used by Solid layer float timeStretch = 1.0f; - uint32_t w = 0, h = 0; + float w = 0.0f, h = 0.0f; float inFrame = 0.0f; float outFrame = 0.0f; float startFrame = 0.0f; @@ -632,19 +582,32 @@ struct LottieLayer : LottieGroup struct LottieSlot { - char* sid; - Array objs; - LottieProperty::Type type; + struct Pair { + LottieObject* obj; + LottieProperty* prop; + }; + + void assign(LottieObject* target); + void reset(); LottieSlot(char* sid, LottieObject* obj, LottieProperty::Type type) : sid(sid), type(type) { - objs.push(obj); + pairs.push({obj, 0}); } ~LottieSlot() { free(sid); + if (!overriden) return; + for (auto pair = pairs.begin(); pair < pairs.end(); ++pair) { + delete(pair->prop); + } } + + char* sid; + Array pairs; + LottieProperty::Type type; + bool overriden = false; }; @@ -664,21 +627,55 @@ struct LottieComposition return p * frameCnt(); } + float timeAtFrame(float frameNo) + { + return (frameNo - startFrame) / frameRate; + } + float frameCnt() const { return endFrame - startFrame; } + LottieLayer* layer(const char* name) + { + for (auto child = root->children.begin(); child < root->children.end(); ++child) { + auto layer = static_cast(*child); + if (layer->name && !strcmp(layer->name, name)) return layer; + } + return nullptr; + } + + LottieLayer* layer(int16_t id) + { + for (auto child = root->children.begin(); child < root->children.end(); ++child) { + auto layer = static_cast(*child); + if (layer->id == id) return layer; + } + return nullptr; + } + + LottieLayer* asset(const char* name) + { + for (auto asset = assets.begin(); asset < assets.end(); ++asset) { + auto layer = static_cast(*asset); + if (layer->name && !strcmp(layer->name, name)) return layer; + } + return nullptr; + } + LottieLayer* root = nullptr; char* version = nullptr; char* name = nullptr; - uint32_t w, h; + float w, h; float startFrame, endFrame; float frameRate; Array assets; Array interpolators; Array fonts; Array slots; + Array markers; + bool expressions = false; bool initiated = false; }; diff --git a/src/libs/thorvg/tvgLottieParser.cpp b/src/libs/thorvg/tvgLottieParser.cpp index 45f14abd5..00890e43a 100644 --- a/src/libs/thorvg/tvgLottieParser.cpp +++ b/src/libs/thorvg/tvgLottieParser.cpp @@ -27,12 +27,31 @@ #include "tvgCompressor.h" #include "tvgLottieModel.h" #include "tvgLottieParser.h" +#include "tvgLottieExpressions.h" /************************************************************************/ /* Internal Class Implementation */ /************************************************************************/ +#define KEY_AS(name) !strcmp(key, name) + + +static LottieExpression* _expression(char* code, LottieComposition* comp, LottieLayer* layer, LottieObject* object, LottieProperty* property, LottieProperty::Type type) +{ + if (!comp->expressions) comp->expressions = true; + + auto inst = new LottieExpression; + inst->code = code; + inst->comp = comp; + inst->layer = layer; + inst->object = object; + inst->property = property; + inst->type = type; + inst->enabled = true; + + return inst; +} static char* _int2str(int num) @@ -45,7 +64,10 @@ static char* _int2str(int num) CompositeMethod LottieParser::getMaskMethod(bool inversed) { - switch (getString()[0]) { + auto mode = getString(); + if (!mode) return CompositeMethod::None; + + switch (mode[0]) { case 'a': { if (inversed) return CompositeMethod::InvAlphaMask; else return CompositeMethod::AddMask; @@ -160,19 +182,19 @@ void LottieParser::getValue(TextDocument& doc) { enterObject(); while (auto key = nextObjectKey()) { - if (!strcmp(key, "s")) doc.size = getFloat(); - else if (!strcmp(key, "f")) doc.name = getStringCopy(); - else if (!strcmp(key, "t")) doc.text = getStringCopy(); - else if (!strcmp(key, "j")) doc.justify = getInt(); - else if (!strcmp(key, "tr")) doc.tracking = getInt(); - else if (!strcmp(key, "lh")) doc.height = getFloat(); - else if (!strcmp(key, "ls")) doc.shift = getFloat(); - else if (!strcmp(key, "fc")) getValue(doc.color); - else if (!strcmp(key, "ps")) getValue(doc.bbox.pos); - else if (!strcmp(key, "sz")) getValue(doc.bbox.size); - else if (!strcmp(key, "sc")) getValue(doc.stroke.color); - else if (!strcmp(key, "sw")) doc.stroke.width = getFloat(); - else if (!strcmp(key, "of")) doc.stroke.render = getBool(); + if (KEY_AS("s")) doc.size = getFloat(); + else if (KEY_AS("f")) doc.name = getStringCopy(); + else if (KEY_AS("t")) doc.text = getStringCopy(); + else if (KEY_AS("j")) doc.justify = getInt(); + else if (KEY_AS("tr")) doc.tracking = getFloat() * 0.1f; + else if (KEY_AS("lh")) doc.height = getFloat(); + else if (KEY_AS("ls")) doc.shift = getFloat(); + else if (KEY_AS("fc")) getValue(doc.color); + else if (KEY_AS("ps")) getValue(doc.bbox.pos); + else if (KEY_AS("sz")) getValue(doc.bbox.size); + else if (KEY_AS("sc")) getValue(doc.stroke.color); + else if (KEY_AS("sw")) doc.stroke.width = getFloat(); + else if (KEY_AS("of")) doc.stroke.render = getBool(); else skip(key); } } @@ -190,10 +212,10 @@ void LottieParser::getValue(PathSet& path) enterObject(); while (auto key = nextObjectKey()) { - if (!strcmp(key, "i")) getValue(ins); - else if (!strcmp(key, "o")) getValue(outs); - else if (!strcmp(key, "v")) getValue(pts); - else if (!strcmp(key, "c")) closed = getBool(); + if (KEY_AS("i")) getValue(ins); + else if (KEY_AS("o")) getValue(outs); + else if (KEY_AS("v")) getValue(pts); + else if (KEY_AS("c")) closed = getBool(); else skip(key); } @@ -255,7 +277,7 @@ void LottieParser::getValue(ColorStop& color) { if (peekType() == kArrayType) enterArray(); - color.input = new Array(context->gradient->colorStops.count); + color.input = new Array(static_cast(context.parent)->colorStops.count); while (nextArrayValue()) color.input->push(getFloat()); } @@ -332,17 +354,17 @@ void LottieParser::getInperpolatorPoint(Point& pt) { enterObject(); while (auto key = nextObjectKey()) { - if (!strcmp(key, "x")) getValue(pt.x); - else if (!strcmp(key, "y")) getValue(pt.y); + if (KEY_AS("x")) getValue(pt.x); + else if (KEY_AS("y")) getValue(pt.y); } } -template +template void LottieParser::parseSlotProperty(T& prop) { while (auto key = nextObjectKey()) { - if (!strcmp(key, "p")) parseProperty(prop); + if (KEY_AS("p")) parseProperty(prop); else skip(key); } } @@ -351,10 +373,10 @@ void LottieParser::parseSlotProperty(T& prop) template bool LottieParser::parseTangent(const char *key, LottieVectorFrame& value) { - if (!strcmp(key, "ti")) { + if (KEY_AS("ti")) { value.hasTangent = true; getValue(value.inTangent); - } else if (!strcmp(key, "to")) { + } else if (KEY_AS("to")) { value.hasTangent = true; getValue(value.outTangent); } else return false; @@ -408,12 +430,12 @@ void LottieParser::parseKeyFrame(T& prop) enterObject(); while (auto key = nextObjectKey()) { - if (!strcmp(key, "i")) { + if (KEY_AS("i")) { interpolator = true; getInperpolatorPoint(inTangent); - } else if (!strcmp(key, "o")) { + } else if (KEY_AS("o")) { getInperpolatorPoint(outTangent); - } else if (!strcmp(key, "n")) { + } else if (KEY_AS("n")) { if (peekType() == kStringType) { interpolatorKey = getString(); } else { @@ -423,18 +445,18 @@ void LottieParser::parseKeyFrame(T& prop) else skip(nullptr); } } - } else if (!strcmp(key, "t")) { + } else if (KEY_AS("t")) { frame.no = getFloat(); - } else if (!strcmp(key, "s")) { + } else if (KEY_AS("s")) { getValue(frame.value); - } else if (!strcmp(key, "e")) { + } else if (KEY_AS("e")) { //current end frame and the next start frame is duplicated, //We propagate the end value to the next frame to avoid having duplicated values. auto& frame2 = prop.nextFrame(); getValue(frame2.value); } else if (parseTangent(key, frame)) { continue; - } else if (!strcmp(key, "h")) { + } else if (KEY_AS("h")) { frame.hold = getInt(); } else skip(key); } @@ -475,17 +497,20 @@ void LottieParser::parseProperty(T& prop, LottieObject* obj) { enterObject(); while (auto key = nextObjectKey()) { - if (!strcmp(key, "k")) parsePropertyInternal(prop); - else if (obj && !strcmp(key, "sid")) { + if (KEY_AS("k")) parsePropertyInternal(prop); + else if (obj && KEY_AS("sid")) { auto sid = getStringCopy(); //append object if the slot already exists. for (auto slot = comp->slots.begin(); slot < comp->slots.end(); ++slot) { if (strcmp((*slot)->sid, sid)) continue; - (*slot)->objs.push(obj); + (*slot)->pairs.push({obj, 0}); return; } comp->slots.push(new LottieSlot(sid, obj, type)); - } else skip(key); + } else if (!strcmp(key, "x")) { + prop.exp = _expression(getStringCopy(), comp, context.layer, context.parent, &prop, type); + } + else skip(key); } } @@ -495,12 +520,14 @@ LottieRect* LottieParser::parseRect() auto rect = new LottieRect; if (!rect) return nullptr; + context.parent = rect; + while (auto key = nextObjectKey()) { - if (!strcmp(key, "s")) parseProperty(rect->size); - else if (!strcmp(key, "p")) parseProperty(rect->position); - else if (!strcmp(key, "r")) parseProperty(rect->radius); - else if (!strcmp(key, "nm")) rect->name = getStringCopy(); - else if (!strcmp(key, "hd")) rect->hidden = getBool(); + if (KEY_AS("s")) parseProperty(rect->size); + else if (KEY_AS("p"))parseProperty(rect->position); + else if (KEY_AS("r")) parseProperty(rect->radius); + else if (KEY_AS("nm")) rect->name = getStringCopy(); + else if (KEY_AS("hd")) rect->hidden = getBool(); else skip(key); } rect->prepare(); @@ -513,11 +540,13 @@ LottieEllipse* LottieParser::parseEllipse() auto ellipse = new LottieEllipse; if (!ellipse) return nullptr; + context.parent = ellipse; + while (auto key = nextObjectKey()) { - if (!strcmp(key, "nm")) ellipse->name = getStringCopy(); - else if (!strcmp(key, "p")) parseProperty(ellipse->position); - else if (!strcmp(key, "s")) parseProperty(ellipse->size); - else if (!strcmp(key, "hd")) ellipse->hidden = getBool(); + if (KEY_AS("nm")) ellipse->name = getStringCopy(); + else if (KEY_AS("p")) parseProperty(ellipse->position); + else if (KEY_AS("s")) parseProperty(ellipse->size); + else if (KEY_AS("hd")) ellipse->hidden = getBool(); else skip(key); } ellipse->prepare(); @@ -530,37 +559,37 @@ LottieTransform* LottieParser::parseTransform(bool ddd) auto transform = new LottieTransform; if (!transform) return nullptr; + context.parent = transform; + if (ddd) { transform->rotationEx = new LottieTransform::RotationEx; TVGLOG("LOTTIE", "3D transform(ddd) is not totally compatible."); } while (auto key = nextObjectKey()) { - if (!strcmp(key, "p")) + if (KEY_AS("p")) { enterObject(); while (auto key = nextObjectKey()) { - if (!strcmp(key, "k")) parsePropertyInternal(transform->position); - else if (!strcmp(key, "s")) { - if (getBool()) transform->coords = new LottieTransform::SeparateCoord; + if (KEY_AS("k")) parsePropertyInternal(transform->position); + else if (KEY_AS("s") && getBool()) transform->coords = new LottieTransform::SeparateCoord; //check separateCoord to figure out whether "x(expression)" / "x(coord)" - } else if (transform->coords && !strcmp(key, "x")) { - parseProperty(transform->coords->x); - } else if (transform->coords && !strcmp(key, "y")) { - parseProperty(transform->coords->y); - } else skip(key); + else if (transform->coords && KEY_AS("x")) parseProperty(transform->coords->x); + else if (transform->coords && KEY_AS("y")) parseProperty(transform->coords->y); + else if (KEY_AS("x")) transform->position.exp = _expression(getStringCopy(), comp, context.layer, context.parent, &transform->position, LottieProperty::Type::Position); + else skip(key); } } - else if (!strcmp(key, "a")) parseProperty(transform->anchor); - else if (!strcmp(key, "s")) parseProperty(transform->scale); - else if (!strcmp(key, "r")) parseProperty(transform->rotation); - else if (!strcmp(key, "o")) parseProperty(transform->opacity); - else if (transform->rotationEx && !strcmp(key, "rx")) parseProperty(transform->rotationEx->x); - else if (transform->rotationEx && !strcmp(key, "ry")) parseProperty(transform->rotationEx->y); - else if (transform->rotationEx && !strcmp(key, "rz")) parseProperty(transform->rotation); - else if (!strcmp(key, "nm")) transform->name = getStringCopy(); - //else if (!strcmp(key, "sk")) //TODO: skew - //else if (!strcmp(key, "sa")) //TODO: skew axis + else if (KEY_AS("a")) parseProperty(transform->anchor); + else if (KEY_AS("s")) parseProperty(transform->scale); + else if (KEY_AS("r")) parseProperty(transform->rotation); + else if (KEY_AS("o")) parseProperty(transform->opacity); + else if (transform->rotationEx && KEY_AS("rx")) parseProperty(transform->rotationEx->x); + else if (transform->rotationEx && KEY_AS("ry")) parseProperty(transform->rotationEx->y); + else if (transform->rotationEx && KEY_AS("rz")) parseProperty(transform->rotation); + else if (KEY_AS("nm")) transform->name = getStringCopy(); + else if (KEY_AS("sk")) parseProperty(transform->skewAngle); + else if (KEY_AS("sa")) parseProperty(transform->skewAxis); else skip(key); } transform->prepare(); @@ -573,13 +602,15 @@ LottieSolidFill* LottieParser::parseSolidFill() auto fill = new LottieSolidFill; if (!fill) return nullptr; + context.parent = fill; + while (auto key = nextObjectKey()) { - if (!strcmp(key, "nm")) fill->name = getStringCopy(); - else if (!strcmp(key, "c")) parseProperty(fill->color, fill); - else if (!strcmp(key, "o")) parseProperty(fill->opacity, fill); - else if (!strcmp(key, "fillEnabled")) fill->hidden |= !getBool(); - else if (!strcmp(key, "r")) fill->rule = getFillRule(); - else if (!strcmp(key, "hd")) fill->hidden = getBool(); + if (KEY_AS("nm")) fill->name = getStringCopy(); + else if (KEY_AS("c")) parseProperty(fill->color, fill); + else if (KEY_AS("o")) parseProperty(fill->opacity, fill); + else if (KEY_AS("fillEnabled")) fill->hidden |= !getBool(); + else if (KEY_AS("r")) fill->rule = getFillRule(); + else if (KEY_AS("hd")) fill->hidden = getBool(); else skip(key); } fill->prepare(); @@ -594,13 +625,13 @@ void LottieParser::parseStrokeDash(LottieStroke* stroke) enterObject(); int idx = 0; while (auto key = nextObjectKey()) { - if (!strcmp(key, "n")) { + if (KEY_AS("n")) { auto style = getString(); if (!strcmp("o", style)) idx = 0; //offset else if (!strcmp("d", style)) idx = 1; //dash else if (!strcmp("g", style)) idx = 2; //gap - } else if (!strcmp(key, "v")) { - parseProperty(stroke->dash(idx)); + } else if (KEY_AS("v")) { + parseProperty(stroke->dash(idx)); } else skip(key); } } @@ -612,17 +643,19 @@ LottieSolidStroke* LottieParser::parseSolidStroke() auto stroke = new LottieSolidStroke; if (!stroke) return nullptr; + context.parent = stroke; + while (auto key = nextObjectKey()) { - if (!strcmp(key, "c")) parseProperty(stroke->color, stroke); - else if (!strcmp(key, "o")) parseProperty(stroke->opacity, stroke); - else if (!strcmp(key, "w")) parseProperty(stroke->width, stroke); - else if (!strcmp(key, "lc")) stroke->cap = getStrokeCap(); - else if (!strcmp(key, "lj")) stroke->join = getStrokeJoin(); - else if (!strcmp(key, "ml")) stroke->miterLimit = getFloat(); - else if (!strcmp(key, "nm")) stroke->name = getStringCopy(); - else if (!strcmp(key, "hd")) stroke->hidden = getBool(); - else if (!strcmp(key, "fillEnabled")) stroke->hidden |= !getBool(); - else if (!strcmp(key, "d")) parseStrokeDash(stroke); + if (KEY_AS("c")) parseProperty(stroke->color, stroke); + else if (KEY_AS("o")) parseProperty(stroke->opacity, stroke); + else if (KEY_AS("w")) parseProperty(stroke->width, stroke); + else if (KEY_AS("lc")) stroke->cap = getStrokeCap(); + else if (KEY_AS("lj")) stroke->join = getStrokeJoin(); + else if (KEY_AS("ml")) stroke->miterLimit = getFloat(); + else if (KEY_AS("nm")) stroke->name = getStringCopy(); + else if (KEY_AS("hd")) stroke->hidden = getBool(); + else if (KEY_AS("fillEnabled")) stroke->hidden |= !getBool(); + else if (KEY_AS("d")) parseStrokeDash(stroke); else skip(key); } stroke->prepare(); @@ -630,17 +663,19 @@ LottieSolidStroke* LottieParser::parseSolidStroke() } - void LottieParser::getPathSet(LottiePathSet& path) +void LottieParser::getPathSet(LottiePathSet& path) { enterObject(); while (auto key = nextObjectKey()) { - if (!strcmp(key, "k")) { + if (KEY_AS("k")) { if (peekType() == kArrayType) { enterArray(); while (nextArrayValue()) parseKeyFrame(path); } else { getValue(path.value); } + } else if (!strcmp(key, "x")) { + path.exp = _expression(getStringCopy(), comp, context.layer, context.parent, &path, LottieProperty::Type::PathSet); } else skip(key); } } @@ -652,9 +687,9 @@ LottiePath* LottieParser::parsePath() if (!path) return nullptr; while (auto key = nextObjectKey()) { - if (!strcmp(key, "nm")) path->name = getStringCopy(); - else if (!strcmp(key, "ks")) getPathSet(path->pathset); - else if (!strcmp(key, "hd")) path->hidden = getBool(); + if (KEY_AS("nm")) path->name = getStringCopy(); + else if (KEY_AS("ks")) getPathSet(path->pathset); + else if (KEY_AS("hd")) path->hidden = getBool(); else skip(key); } path->prepare(); @@ -667,17 +702,19 @@ LottiePolyStar* LottieParser::parsePolyStar() auto star = new LottiePolyStar; if (!star) return nullptr; + context.parent = star; + while (auto key = nextObjectKey()) { - if (!strcmp(key, "nm")) star->name = getStringCopy(); - else if (!strcmp(key, "p")) parseProperty(star->position); - else if (!strcmp(key, "pt")) parseProperty(star->ptsCnt); - else if (!strcmp(key, "ir")) parseProperty(star->innerRadius); - else if (!strcmp(key, "is")) parseProperty(star->innerRoundness); - else if (!strcmp(key, "or")) parseProperty(star->outerRadius); - else if (!strcmp(key, "os")) parseProperty(star->outerRoundness); - else if (!strcmp(key, "r")) parseProperty(star->rotation); - else if (!strcmp(key, "sy")) star->type = (LottiePolyStar::Type) getInt(); - else if (!strcmp(key, "hd")) star->hidden = getBool(); + if (KEY_AS("nm")) star->name = getStringCopy(); + else if (KEY_AS("p")) parseProperty(star->position); + else if (KEY_AS("pt")) parseProperty(star->ptsCnt); + else if (KEY_AS("ir")) parseProperty(star->innerRadius); + else if (KEY_AS("is")) parseProperty(star->innerRoundness); + else if (KEY_AS("or")) parseProperty(star->outerRadius); + else if (KEY_AS("os")) parseProperty(star->outerRoundness); + else if (KEY_AS("r")) parseProperty(star->rotation); + else if (KEY_AS("sy")) star->type = (LottiePolyStar::Type) getInt(); + else if (KEY_AS("hd")) star->hidden = getBool(); else skip(key); } star->prepare(); @@ -690,10 +727,12 @@ LottieRoundedCorner* LottieParser::parseRoundedCorner() auto corner = new LottieRoundedCorner; if (!corner) return nullptr; + context.parent = corner; + while (auto key = nextObjectKey()) { - if (!strcmp(key, "nm")) corner->name = getStringCopy(); - else if (!strcmp(key, "r")) parseProperty(corner->radius); - else if (!strcmp(key, "hd")) corner->hidden = getBool(); + if (KEY_AS("nm")) corner->name = getStringCopy(); + else if (KEY_AS("r")) parseProperty(corner->radius); + else if (KEY_AS("hd")) corner->hidden = getBool(); else skip(key); } corner->prepare(); @@ -703,23 +742,21 @@ LottieRoundedCorner* LottieParser::parseRoundedCorner() void LottieParser::parseGradient(LottieGradient* gradient, const char* key) { - context->gradient = gradient; - - if (!strcmp(key, "t")) gradient->id = getInt(); - else if (!strcmp(key, "o")) parseProperty(gradient->opacity, gradient); - else if (!strcmp(key, "g")) + if (KEY_AS("t")) gradient->id = getInt(); + else if (KEY_AS("o")) parseProperty(gradient->opacity, gradient); + else if (KEY_AS("g")) { enterObject(); while (auto key = nextObjectKey()) { - if (!strcmp(key, "p")) gradient->colorStops.count = getInt(); - else if (!strcmp(key, "k")) parseProperty(gradient->colorStops, gradient); + if (KEY_AS("p")) gradient->colorStops.count = getInt(); + else if (KEY_AS("k")) parseProperty(gradient->colorStops, gradient); else skip(key); } } - else if (!strcmp(key, "s")) parseProperty(gradient->start, gradient); - else if (!strcmp(key, "e")) parseProperty(gradient->end, gradient); - else if (!strcmp(key, "h")) parseProperty(gradient->height, gradient); - else if (!strcmp(key, "a")) parseProperty(gradient->angle, gradient); + else if (KEY_AS("s")) parseProperty(gradient->start, gradient); + else if (KEY_AS("e")) parseProperty(gradient->end, gradient); + else if (KEY_AS("h")) parseProperty(gradient->height, gradient); + else if (KEY_AS("a")) parseProperty(gradient->angle, gradient); else skip(key); } @@ -729,10 +766,12 @@ LottieGradientFill* LottieParser::parseGradientFill() auto fill = new LottieGradientFill; if (!fill) return nullptr; + context.parent = fill; + while (auto key = nextObjectKey()) { - if (!strcmp(key, "nm")) fill->name = getStringCopy(); - else if (!strcmp(key, "r")) fill->rule = getFillRule(); - else if (!strcmp(key, "hd")) fill->hidden = getBool(); + if (KEY_AS("nm")) fill->name = getStringCopy(); + else if (KEY_AS("r")) fill->rule = getFillRule(); + else if (KEY_AS("hd")) fill->hidden = getBool(); else parseGradient(fill, key); } @@ -747,14 +786,16 @@ LottieGradientStroke* LottieParser::parseGradientStroke() auto stroke = new LottieGradientStroke; if (!stroke) return nullptr; + context.parent = stroke; + while (auto key = nextObjectKey()) { - if (!strcmp(key, "nm")) stroke->name = getStringCopy(); - else if (!strcmp(key, "lc")) stroke->cap = getStrokeCap(); - else if (!strcmp(key, "lj")) stroke->join = getStrokeJoin(); - else if (!strcmp(key, "ml")) stroke->miterLimit = getFloat(); - else if (!strcmp(key, "hd")) stroke->hidden = getBool(); - else if (!strcmp(key, "w")) parseProperty(stroke->width); - else if (!strcmp(key, "d")) parseStrokeDash(stroke); + if (KEY_AS("nm")) stroke->name = getStringCopy(); + else if (KEY_AS("lc")) stroke->cap = getStrokeCap(); + else if (KEY_AS("lj")) stroke->join = getStrokeJoin(); + else if (KEY_AS("ml")) stroke->miterLimit = getFloat(); + else if (KEY_AS("hd")) stroke->hidden = getBool(); + else if (KEY_AS("w")) parseProperty(stroke->width); + else if (KEY_AS("d")) parseStrokeDash(stroke); else parseGradient(stroke, key); } stroke->prepare(); @@ -768,13 +809,15 @@ LottieTrimpath* LottieParser::parseTrimpath() auto trim = new LottieTrimpath; if (!trim) return nullptr; + context.parent = trim; + while (auto key = nextObjectKey()) { - if (!strcmp(key, "nm")) trim->name = getStringCopy(); - else if (!strcmp(key, "s")) parseProperty(trim->start); - else if (!strcmp(key, "e")) parseProperty(trim->end); - else if (!strcmp(key, "o")) parseProperty(trim->offset); - else if (!strcmp(key, "m")) trim->type = static_cast(getInt()); - else if (!strcmp(key, "hd")) trim->hidden = getBool(); + if (KEY_AS("nm")) trim->name = getStringCopy(); + else if (KEY_AS("s")) parseProperty(trim->start); + else if (KEY_AS("e")) parseProperty(trim->end); + else if (KEY_AS("o")) parseProperty(trim->offset); + else if (KEY_AS("m")) trim->type = static_cast(getInt()); + else if (KEY_AS("hd")) trim->hidden = getBool(); else skip(key); } trim->prepare(); @@ -788,25 +831,27 @@ LottieRepeater* LottieParser::parseRepeater() auto repeater = new LottieRepeater; if (!repeater) return nullptr; + context.parent = repeater; + while (auto key = nextObjectKey()) { - if (!strcmp(key, "nm")) repeater->name = getStringCopy(); - else if (!strcmp(key, "c")) parseProperty(repeater->copies); - else if (!strcmp(key, "o")) parseProperty(repeater->offset); - else if (!strcmp(key, "m")) repeater->inorder = getInt(); - else if (!strcmp(key, "tr")) + if (KEY_AS("nm")) repeater->name = getStringCopy(); + else if (KEY_AS("c")) parseProperty(repeater->copies); + else if (KEY_AS("o")) parseProperty(repeater->offset); + else if (KEY_AS("m")) repeater->inorder = getInt(); + else if (KEY_AS("tr")) { enterObject(); - while (auto key2 = nextObjectKey()) { - if (!strcmp(key2, "a")) parseProperty(repeater->anchor); - else if (!strcmp(key2, "p")) parseProperty(repeater->position); - else if (!strcmp(key2, "r")) parseProperty(repeater->rotation); - else if (!strcmp(key2, "s")) parseProperty(repeater->scale); - else if (!strcmp(key2, "so")) parseProperty(repeater->startOpacity); - else if (!strcmp(key2, "eo")) parseProperty(repeater->endOpacity); - else skip(key2); + while (auto key = nextObjectKey()) { + if (KEY_AS("a")) parseProperty(repeater->anchor); + else if (KEY_AS("p")) parseProperty(repeater->position); + else if (KEY_AS("r")) parseProperty(repeater->rotation); + else if (KEY_AS("s")) parseProperty(repeater->scale); + else if (KEY_AS("so")) parseProperty(repeater->startOpacity); + else if (KEY_AS("eo")) parseProperty(repeater->endOpacity); + else skip(key); } } - else if (!strcmp(key, "hd")) repeater->hidden = getBool(); + else if (KEY_AS("hd")) repeater->hidden = getBool(); else skip(key); } repeater->prepare(); @@ -846,7 +891,7 @@ void LottieParser::parseObject(Array& parent) { enterObject(); while (auto key = nextObjectKey()) { - if (!strcmp(key, "ty")) { + if (KEY_AS("ty")) { if (auto child = parseObject()) { if (child->hidden) delete(child); else parent.push(child); @@ -898,7 +943,7 @@ LottieObject* LottieParser::parseAsset() auto embedded = false; while (auto key = nextObjectKey()) { - if (!strcmp(key, "id")) + if (KEY_AS("id")) { if (peekType() == kStringType) { id = getStringCopy(); @@ -906,14 +951,15 @@ LottieObject* LottieParser::parseAsset() id = _int2str(getInt()); } } - else if (!strcmp(key, "layers")) obj = parseLayers(); - else if (!strcmp(key, "u")) subPath = getString(); - else if (!strcmp(key, "p")) data = getString(); - else if (!strcmp(key, "e")) embedded = getInt(); + else if (KEY_AS("layers")) obj = parseLayers(); + else if (KEY_AS("u")) subPath = getString(); + else if (KEY_AS("p")) data = getString(); + else if (KEY_AS("e")) embedded = getInt(); else skip(key); } if (data) obj = parseImage(data, subPath, embedded); if (obj) obj->name = id; + else free(id); return obj; } @@ -925,11 +971,11 @@ LottieFont* LottieParser::parseFont() auto font = new LottieFont; while (auto key = nextObjectKey()) { - if (!strcmp(key, "fName")) font->name = getStringCopy(); - else if (!strcmp(key, "fFamily")) font->family = getStringCopy(); - else if (!strcmp(key, "fStyle")) font->style = getStringCopy(); - else if (!strcmp(key, "ascent")) font->ascent = getFloat(); - else if (!strcmp(key, "origin")) font->origin = (LottieFont::Origin) getInt(); + if (KEY_AS("fName")) font->name = getStringCopy(); + else if (KEY_AS("fFamily")) font->family = getStringCopy(); + else if (KEY_AS("fStyle")) font->style = getStringCopy(); + else if (KEY_AS("ascent")) font->ascent = getFloat(); + else if (KEY_AS("origin")) font->origin = (LottieFont::Origin) getInt(); else skip(key); } return font; @@ -946,6 +992,29 @@ void LottieParser::parseAssets() } } +LottieMarker* LottieParser::parseMarker() +{ + enterObject(); + + auto marker = new LottieMarker; + + while (auto key = nextObjectKey()) { + if (KEY_AS("cm")) marker->name = getStringCopy(); + else if (KEY_AS("tm")) marker->time = getFloat(); + else if (KEY_AS("dr")) marker->duration = getFloat(); + else skip(key); + } + + return marker; +} + +void LottieParser::parseMarkers() +{ + enterArray(); + while (nextArrayValue()) { + comp->markers.push(parseMarker()); + } +} void LottieParser::parseChars(Array& glyphes) { @@ -955,16 +1024,16 @@ void LottieParser::parseChars(Array& glyphes) //a new glyph auto glyph = new LottieGlyph; while (auto key = nextObjectKey()) { - if (!strcmp("ch", key)) glyph->code = getStringCopy(); - else if (!strcmp("size", key)) glyph->size = static_cast(getFloat()); - else if (!strcmp("style", key)) glyph->style = getStringCopy(); - else if (!strcmp("w", key)) glyph->width = getFloat(); - else if (!strcmp("fFamily", key)) glyph->family = getStringCopy(); - else if (!strcmp("data", key)) + if (KEY_AS("ch")) glyph->code = getStringCopy(); + else if (KEY_AS("size")) glyph->size = static_cast(getFloat()); + else if (KEY_AS("style")) glyph->style = getStringCopy(); + else if (KEY_AS("w")) glyph->width = getFloat(); + else if (KEY_AS("fFamily")) glyph->family = getStringCopy(); + else if (KEY_AS("data")) { //glyph shapes enterObject(); while (auto key = nextObjectKey()) { - if (!strcmp(key, "shapes")) parseShapes(glyph->children); + if (KEY_AS("shapes")) parseShapes(glyph->children); } } else skip(key); } @@ -977,7 +1046,7 @@ void LottieParser::parseFonts() { enterObject(); while (auto key = nextObjectKey()) { - if (!strcmp("list", key)) { + if (KEY_AS("list")) { enterArray(); while (nextArrayValue()) { comp->fonts.push(parseFont()); @@ -993,9 +1062,9 @@ LottieObject* LottieParser::parseGroup() if (!group) return nullptr; while (auto key = nextObjectKey()) { - if (!strcmp(key, "nm")) { + if (KEY_AS("nm")) { group->name = getStringCopy(); - } else if (!strcmp(key, "it")) { + } else if (KEY_AS("it")) { enterArray(); while (nextArrayValue()) parseObject(group->children); } else skip(key); @@ -1012,7 +1081,7 @@ LottieObject* LottieParser::parseGroup() void LottieParser::parseTimeRemap(LottieLayer* layer) { - parseProperty(layer->timeRemap); + parseProperty(layer->timeRemap); } @@ -1034,12 +1103,12 @@ void LottieParser::parseShapes(Array& parent) direction = 0; enterObject(); while (auto key = nextObjectKey()) { - if (!strcmp(key, "it")) { + if (KEY_AS("it")) { enterArray(); while (nextArrayValue()) parseObject(parent); - } else if (!strcmp(key, "d")) { + } else if (KEY_AS("d")) { direction = getDirection(); - } else if (!strcmp(key, "ty")) { + } else if (KEY_AS("ty")) { if (auto child = parseObject()) { if (child->hidden) delete(child); else parent.push(child); @@ -1056,14 +1125,14 @@ void LottieParser::parseTextRange(LottieText* text) enterArray(); while (nextArrayValue()) { enterObject(); - while (auto key2 = nextObjectKey()) { - if (!strcmp(key2, "a")) { //text style + while (auto key = nextObjectKey()) { + if (KEY_AS("a")) { //text style enterObject(); - while (auto key3 = nextObjectKey()) { - if (!strcmp(key3, "t")) parseProperty(text->spacing); - else skip(key3); + while (auto key = nextObjectKey()) { + if (KEY_AS("t")) parseProperty(text->spacing); + else skip(key); } - } else skip(key2); + } else skip(key); } } } @@ -1076,10 +1145,10 @@ void LottieParser::parseText(Array& parent) auto text = new LottieText; while (auto key = nextObjectKey()) { - if (!strcmp(key, "d")) parseProperty(text->doc, text); - else if (!strcmp(key, "a")) parseTextRange(text); - //else if (!strcmp(key, "p")) TVGLOG("LOTTIE", "Text Follow Path (p) is not supported"); - //else if (!strcmp(key, "m")) TVGLOG("LOTTIE", "Text Alignment Option (m) is not supported"); + if (KEY_AS("d")) parseProperty(text->doc, text); + else if (KEY_AS("a")) parseTextRange(text); + //else if (KEY_AS("p")) TVGLOG("LOTTIE", "Text Follow Path (p) is not supported"); + //else if (KEY_AS("m")) TVGLOG("LOTTIE", "Text Alignment Option (m) is not supported"); else skip(key); } @@ -1088,14 +1157,14 @@ void LottieParser::parseText(Array& parent) } -void LottieParser::getLayerSize(uint32_t& val) +void LottieParser::getLayerSize(float& val) { - if (val == 0) { - val = getInt(); + if (val == 0.0f) { + val = getFloat(); } else { //layer might have both w(width) & sw(solid color width) //override one if the a new size is smaller. - uint32_t w = getInt(); + auto w = getFloat(); if (w < val) val = w; } } @@ -1107,10 +1176,10 @@ LottieMask* LottieParser::parseMask() enterObject(); while (auto key = nextObjectKey()) { - if (!strcmp(key, "inv")) mask->inverse = getBool(); - else if (!strcmp(key, "mode")) mask->method = getMaskMethod(mask->inverse); - else if (!strcmp(key, "pt")) getPathSet(mask->pathset); - else if (!strcmp(key, "o")) parseProperty(mask->opacity); + if (KEY_AS("inv")) mask->inverse = getBool(); + else if (KEY_AS("mode")) mask->method = getMaskMethod(mask->inverse); + else if (KEY_AS("pt")) getPathSet(mask->pathset); + else if (KEY_AS("o")) parseProperty(mask->opacity); else skip(key); } @@ -1123,7 +1192,6 @@ void LottieParser::parseMasks(LottieLayer* layer) enterArray(); while (nextArrayValue()) { auto mask = parseMask(); - if (mask->dynamic()) layer->statical = false; layer->masks.push(mask); } } @@ -1135,41 +1203,41 @@ LottieLayer* LottieParser::parseLayer() if (!layer) return nullptr; layer->comp = comp; - context->layer = layer; + context.layer = layer; auto ddd = false; enterObject(); while (auto key = nextObjectKey()) { - if (!strcmp(key, "ddd")) ddd = getInt(); //3d layer - else if (!strcmp(key, "ind")) layer->id = getInt(); - else if (!strcmp(key, "ty")) layer->type = (LottieLayer::Type) getInt(); - else if (!strcmp(key, "nm")) layer->name = getStringCopy(); - else if (!strcmp(key, "sr")) layer->timeStretch = getFloat(); - else if (!strcmp(key, "ks")) + if (KEY_AS("ddd")) ddd = getInt(); //3d layer + else if (KEY_AS("ind")) layer->id = getInt(); + else if (KEY_AS("ty")) layer->type = (LottieLayer::Type) getInt(); + else if (KEY_AS("nm")) layer->name = getStringCopy(); + else if (KEY_AS("sr")) layer->timeStretch = getFloat(); + else if (KEY_AS("ks")) { enterObject(); layer->transform = parseTransform(ddd); } - else if (!strcmp(key, "ao")) layer->autoOrient = getInt(); - else if (!strcmp(key, "shapes")) parseShapes(layer->children); - else if (!strcmp(key, "ip")) layer->inFrame = getFloat(); - else if (!strcmp(key, "op")) layer->outFrame = getFloat(); - else if (!strcmp(key, "st")) layer->startFrame = getFloat(); - else if (!strcmp(key, "bm")) layer->blendMethod = getBlendMethod(); - else if (!strcmp(key, "parent")) layer->pid = getInt(); - else if (!strcmp(key, "tm")) parseTimeRemap(layer); - else if (!strcmp(key, "w") || !strcmp(key, "sw")) getLayerSize(layer->w); - else if (!strcmp(key, "h") || !strcmp(key, "sh")) getLayerSize(layer->h); - else if (!strcmp(key, "sc")) layer->color = getColor(getString()); - else if (!strcmp(key, "tt")) layer->matte.type = getMatteType(); - else if (!strcmp(key, "masksProperties")) parseMasks(layer); - else if (!strcmp(key, "hd")) layer->hidden = getBool(); - else if (!strcmp(key, "refId")) layer->refId = getStringCopy(); - else if (!strcmp(key, "td")) layer->matteSrc = getInt(); //used for matte layer - else if (!strcmp(key, "t")) parseText(layer->children); - else if (!strcmp(key, "ef")) + else if (KEY_AS("ao")) layer->autoOrient = getInt(); + else if (KEY_AS("shapes")) parseShapes(layer->children); + else if (KEY_AS("ip")) layer->inFrame = getFloat(); + else if (KEY_AS("op")) layer->outFrame = getFloat(); + else if (KEY_AS("st")) layer->startFrame = getFloat(); + else if (KEY_AS("bm")) layer->blendMethod = getBlendMethod(); + else if (KEY_AS("parent")) layer->pid = getInt(); + else if (KEY_AS("tm")) parseTimeRemap(layer); + else if (KEY_AS("w") || KEY_AS("sw")) getLayerSize(layer->w); + else if (KEY_AS("h") || KEY_AS("sh")) getLayerSize(layer->h); + else if (KEY_AS("sc")) layer->color = getColor(getString()); + else if (KEY_AS("tt")) layer->matte.type = getMatteType(); + else if (KEY_AS("masksProperties")) parseMasks(layer); + else if (KEY_AS("hd")) layer->hidden = getBool(); + else if (KEY_AS("refId")) layer->refId = getStringCopy(); + else if (KEY_AS("td")) layer->matteSrc = getInt(); //used for matte layer + else if (KEY_AS("t")) parseText(layer->children); + else if (KEY_AS("ef")) { TVGERR("LOTTIE", "layer effect(ef) is not supported!"); skip(key); @@ -1241,38 +1309,41 @@ void LottieParser::postProcess(Array& glyphes) /* External Class Implementation */ /************************************************************************/ -const char* LottieParser::sid() +const char* LottieParser::sid(bool first) { - //verify json - if (!parseNext()) return nullptr; - enterObject(); + if (first) { + //verify json + if (!parseNext()) return nullptr; + enterObject(); + } return nextObjectKey(); } -bool LottieParser::parse(LottieSlot* slot) +bool LottieParser::apply(LottieSlot* slot) { enterObject(); - LottieParser::Context context; - this->context = &context; + //OPTIMIZE: we can create the property directly, without object LottieObject* obj = nullptr; //slot object switch (slot->type) { case LottieProperty::Type::ColorStop: { obj = new LottieGradient; - context.gradient = static_cast(obj); - parseSlotProperty(static_cast(obj)->colorStops); + context.parent = obj; + parseSlotProperty(static_cast(obj)->colorStops); break; } case LottieProperty::Type::Color: { obj = new LottieSolid; - parseSlotProperty(static_cast(obj)->color); + context.parent = obj; + parseSlotProperty(static_cast(obj)->color); break; } case LottieProperty::Type::TextDoc: { obj = new LottieText; - parseSlotProperty(static_cast(obj)->doc); + context.parent = obj; + parseSlotProperty(static_cast(obj)->doc); break; } default: break; @@ -1280,10 +1351,7 @@ bool LottieParser::parse(LottieSlot* slot) if (!obj || Invalid()) return false; - //apply slot object to all targets - for (auto target = slot->objs.begin(); target < slot->objs.end(); ++target) { - (*target)->override(obj); - } + slot->assign(obj); delete(obj); @@ -1304,26 +1372,26 @@ bool LottieParser::parse() Array glyphes; - //assign parsing context - LottieParser::Context context; - this->context = &context; - while (auto key = nextObjectKey()) { - if (!strcmp(key, "v")) comp->version = getStringCopy(); - else if (!strcmp(key, "fr")) comp->frameRate = getFloat(); - else if (!strcmp(key, "ip")) comp->startFrame = getFloat(); - else if (!strcmp(key, "op")) comp->endFrame = getFloat(); - else if (!strcmp(key, "w")) comp->w = getInt(); - else if (!strcmp(key, "h")) comp->h = getInt(); - else if (!strcmp(key, "nm")) comp->name = getStringCopy(); - else if (!strcmp(key, "assets")) parseAssets(); - else if (!strcmp(key, "layers")) comp->root = parseLayers(); - else if (!strcmp(key, "fonts")) parseFonts(); - else if (!strcmp(key, "chars")) parseChars(glyphes); + if (KEY_AS("v")) comp->version = getStringCopy(); + else if (KEY_AS("fr")) comp->frameRate = getFloat(); + else if (KEY_AS("ip")) comp->startFrame = getFloat(); + else if (KEY_AS("op")) comp->endFrame = getFloat(); + else if (KEY_AS("w")) comp->w = getFloat(); + else if (KEY_AS("h")) comp->h = getFloat(); + else if (KEY_AS("nm")) comp->name = getStringCopy(); + else if (KEY_AS("assets")) parseAssets(); + else if (KEY_AS("layers")) comp->root = parseLayers(); + else if (KEY_AS("fonts")) parseFonts(); + else if (KEY_AS("chars")) parseChars(glyphes); + else if (KEY_AS("markers")) parseMarkers(); else skip(key); } - if (Invalid() || !comp->root) return false; + if (Invalid() || !comp->root) { + delete(comp); + return false; + } comp->root->inFrame = comp->startFrame; comp->root->outFrame = comp->endFrame; diff --git a/src/libs/thorvg/tvgLottieParser.h b/src/libs/thorvg/tvgLottieParser.h index b7b5553c9..9192e6502 100644 --- a/src/libs/thorvg/tvgLottieParser.h +++ b/src/libs/thorvg/tvgLottieParser.h @@ -39,8 +39,8 @@ public: } bool parse(); - bool parse(LottieSlot* slot); - const char* sid(); + bool apply(LottieSlot* slot); + const char* sid(bool first = false); LottieComposition* comp = nullptr; const char* dirName = nullptr; //base resource directory @@ -58,7 +58,7 @@ private: void getInperpolatorPoint(Point& pt); void getPathSet(LottiePathSet& path); - void getLayerSize(uint32_t& val); + void getLayerSize(float& val); void getValue(TextDocument& doc); void getValue(PathSet& path); void getValue(Array& pts); @@ -73,7 +73,7 @@ private: template void parseKeyFrame(T& prop); template void parsePropertyInternal(T& prop); template void parseProperty(T& prop, LottieObject* obj = nullptr); - template void parseSlotProperty(T& prop); + template void parseSlotProperty(T& prop); LottieObject* parseObject(); LottieObject* parseAsset(); @@ -95,6 +95,7 @@ private: LottieTrimpath* parseTrimpath(); LottieRepeater* parseRepeater(); LottieFont* parseFont(); + LottieMarker* parseMarker(); void parseObject(Array& parent); void parseShapes(Array& parent); @@ -107,13 +108,14 @@ private: void parseAssets(); void parseFonts(); void parseChars(Array& glyphes); + void parseMarkers(); void postProcess(Array& glyphes); //Current parsing context struct Context { LottieLayer* layer = nullptr; - LottieGradient* gradient = nullptr; - } *context; + LottieObject* parent = nullptr; + } context; }; #endif //_TVG_LOTTIE_PARSER_H_ diff --git a/src/libs/thorvg/tvgLottieParserHandler.h b/src/libs/thorvg/tvgLottieParserHandler.h index ac7c049f6..65b4545a2 100644 --- a/src/libs/thorvg/tvgLottieParserHandler.h +++ b/src/libs/thorvg/tvgLottieParserHandler.h @@ -51,8 +51,6 @@ #include "rapidjson/document.h" #include "tvgCommon.h" -RAPIDJSON_DIAG_PUSH -RAPIDJSON_DIAG_OFF(effc++) using namespace rapidjson; @@ -133,7 +131,7 @@ struct LookaheadParserHandler } bool RawNumber(const char *, SizeType, TVG_UNUSED bool) - { + { return false; } diff --git a/src/libs/thorvg/tvgLottieProperty.h b/src/libs/thorvg/tvgLottieProperty.h index 4525e5ad1..19df89529 100644 --- a/src/libs/thorvg/tvgLottieProperty.h +++ b/src/libs/thorvg/tvgLottieProperty.h @@ -29,8 +29,16 @@ #include "tvgCommon.h" #include "tvgArray.h" #include "tvgMath.h" -#include "tvgBezier.h" +#include "tvgLines.h" #include "tvgLottieInterpolator.h" +#include "tvgLottieExpressions.h" + +#define ROUNDNESS_EPSILON 1.0f + +struct LottieFont; +struct LottieLayer; +struct LottieObject; + struct PathSet { @@ -54,8 +62,6 @@ struct ColorStop }; -struct LottieFont; - struct TextDocument { char* text = nullptr; @@ -73,8 +79,8 @@ struct TextDocument } stroke; char* name = nullptr; float size; + float tracking = 0.0f; uint8_t justify; - uint8_t tracking; }; @@ -96,26 +102,6 @@ static inline RGB24 operator*(const RGB24& lhs, float rhs) } -static void copy(PathSet& pathset, Array& outPts) -{ - Array inPts; - inPts.data = pathset.pts; - inPts.count = pathset.ptsCnt; - outPts.push(inPts); - inPts.data = nullptr; -} - - -static void copy(PathSet& pathset, Array& outCmds) -{ - Array inCmds; - inCmds.data = pathset.cmds; - inCmds.count = pathset.cmdsCnt; - outCmds.push(inCmds); - inCmds.data = nullptr; -} - - template struct LottieScalarFrame { @@ -186,28 +172,208 @@ struct LottieVectorFrame }; -template -uint32_t bsearch(T* frames, float frameNo) +//Property would have an either keyframes or single value. +struct LottieProperty { - uint32_t low = 0; - uint32_t high = frames->count - 1; + enum class Type : uint8_t { Point = 0, Float, Opacity, Color, PathSet, ColorStop, Position, TextDoc, Invalid }; + virtual ~LottieProperty() {} + + LottieExpression* exp = nullptr; + + //TODO: Apply common bodies? + virtual uint32_t frameCnt() = 0; + virtual uint32_t nearest(float time) = 0; + virtual float frameNo(int32_t key) = 0; +}; + + +struct LottieExpression +{ + enum LoopMode : uint8_t { None = 0, InCycle = 1, InPingPong, InOffset, InContinue, OutCycle, OutPingPong, OutOffset, OutContinue }; + + char* code; + LottieComposition* comp; + LottieLayer* layer; + LottieObject* object; + LottieProperty* property; + LottieProperty::Type type; + + bool enabled; + + struct { + uint32_t key = 0; //the keyframe number repeating to + float in = FLT_MAX; //looping duration in frame number + LoopMode mode = None; + } loop; +; + ~LottieExpression() + { + free(code); + } +}; + + +static void _copy(PathSet* pathset, Array& outPts, Matrix* transform) +{ + Array inPts; + + if (transform) { + for (int i = 0; i < pathset->ptsCnt; ++i) { + Point pt = pathset->pts[i]; + mathMultiply(&pt, transform); + outPts.push(pt); + } + } else { + inPts.data = pathset->pts; + inPts.count = pathset->ptsCnt; + outPts.push(inPts); + inPts.data = nullptr; + } +} + + +static void _copy(PathSet* pathset, Array& outCmds) +{ + Array inCmds; + inCmds.data = pathset->cmds; + inCmds.count = pathset->cmdsCnt; + outCmds.push(inCmds); + inCmds.data = nullptr; +} + + +static void _roundCorner(Array& cmds, Array& pts, const Point& prev, const Point& curr, const Point& next, float roundness) +{ + auto lenPrev = mathLength(prev - curr); + auto rPrev = lenPrev > 0.0f ? 0.5f * mathMin(lenPrev * 0.5f, roundness) / lenPrev : 0.0f; + auto lenNext = mathLength(next - curr); + auto rNext = lenNext > 0.0f ? 0.5f * mathMin(lenNext * 0.5f, roundness) / lenNext : 0.0f; + + auto dPrev = rPrev * (curr - prev); + auto dNext = rNext * (curr - next); + + pts.push(curr - 2.0f * dPrev); + pts.push(curr - dPrev); + pts.push(curr - dNext); + pts.push(curr - 2.0f * dNext); + cmds.push(PathCommand::LineTo); + cmds.push(PathCommand::CubicTo); +} + + +static bool _modifier(Point* inputPts, uint32_t inputPtsCnt, PathCommand* inputCmds, uint32_t inputCmdsCnt, Array& cmds, Array& pts, Matrix* transform, float roundness) +{ + cmds.reserve(inputCmdsCnt * 2); + pts.reserve((uint16_t)(inputPtsCnt * 1.5)); + auto ptsCnt = pts.count; + + auto startIndex = 0; + for (uint32_t iCmds = 0, iPts = 0; iCmds < inputCmdsCnt; ++iCmds) { + switch (inputCmds[iCmds]) { + case PathCommand::MoveTo: { + startIndex = pts.count; + cmds.push(PathCommand::MoveTo); + pts.push(inputPts[iPts++]); + break; + } + case PathCommand::CubicTo: { + auto& prev = inputPts[iPts - 1]; + auto& curr = inputPts[iPts + 2]; + if (iCmds < inputCmdsCnt - 1 && + mathZero(inputPts[iPts - 1] - inputPts[iPts]) && + mathZero(inputPts[iPts + 1] - inputPts[iPts + 2])) { + if (inputCmds[iCmds + 1] == PathCommand::CubicTo && + mathZero(inputPts[iPts + 2] - inputPts[iPts + 3]) && + mathZero(inputPts[iPts + 4] - inputPts[iPts + 5])) { + _roundCorner(cmds, pts, prev, curr, inputPts[iPts + 5], roundness); + iPts += 3; + break; + } else if (inputCmds[iCmds + 1] == PathCommand::Close) { + _roundCorner(cmds, pts, prev, curr, inputPts[2], roundness); + pts[startIndex] = pts.last(); + iPts += 3; + break; + } + } + cmds.push(PathCommand::CubicTo); + pts.push(inputPts[iPts++]); + pts.push(inputPts[iPts++]); + pts.push(inputPts[iPts++]); + break; + } + case PathCommand::Close: { + cmds.push(PathCommand::Close); + break; + } + default: break; + } + } + if (transform) { + for (auto i = ptsCnt; i < pts.count; ++i) + mathTransform(transform, &pts[i]); + } + return true; +} + + +template +uint32_t _bsearch(T* frames, float frameNo) +{ + int32_t low = 0; + int32_t high = int32_t(frames->count) - 1; while (low <= high) { auto mid = low + (high - low) / 2; auto frame = frames->data + mid; - if (mathEqual(frameNo, frame->no)) return mid; - else if (frameNo < frame->no) high = mid - 1; + if (frameNo < frame->no) high = mid - 1; else low = mid + 1; } if (high < low) low = high; + if (low < 0) low = 0; return low; } -struct LottieProperty +template +uint32_t _nearest(T* frames, float frameNo) { - enum class Type : uint8_t { Point = 0, Float, Opacity, Color, PathSet, ColorStop, Position, TextDoc, Invalid }; -}; + if (frames) { + auto key = _bsearch(frames, frameNo); + if (key == frames->count - 1) return key; + return (fabsf(frames->data[key].no - frameNo) < fabsf(frames->data[key + 1].no - frameNo)) ? key : (key + 1); + } + return 0; +} + + +template +float _frameNo(T* frames, int32_t key) +{ + if (!frames) return 0.0f; + if (key < 0) key = 0; + if (key >= (int32_t) frames->count) key = (int32_t)(frames->count - 1); + return (*frames)[key].no; +} + + +template +float _loop(T* frames, float frameNo, LottieExpression* exp) +{ + if (frameNo >= exp->loop.in || frameNo < frames->first().no ||frameNo < frames->last().no) return frameNo; + + switch (exp->loop.mode) { + case LottieExpression::LoopMode::InCycle: { + frameNo -= frames->first().no; + return fmodf(frameNo, frames->last().no - frames->first().no) + (*frames)[exp->loop.key].no; + } + case LottieExpression::LoopMode::OutCycle: { + frameNo -= frames->first().no; + return fmodf(frameNo, (*frames)[frames->count - 1 - exp->loop.key].no - frames->first().no) + frames->first().no; + } + default: break; + } + return frameNo; +} template @@ -218,10 +384,36 @@ struct LottieGenericProperty : LottieProperty T value; LottieGenericProperty(T v) : value(v) {} + LottieGenericProperty() {} ~LottieGenericProperty() + { + release(); + } + + void release() { delete(frames); + frames = nullptr; + if (exp) { + delete(exp); + exp = nullptr; + } + } + + uint32_t nearest(float frameNo) override + { + return _nearest(frames, frameNo); + } + + uint32_t frameCnt() override + { + return frames ? frames->count : 1; + } + + float frameNo(int32_t key) override + { + return _frameNo(frames, key); } LottieScalarFrame& newFrame() @@ -247,15 +439,24 @@ struct LottieGenericProperty : LottieProperty if (frames->count == 1 || frameNo <= frames->first().no) return frames->first().value; if (frameNo >= frames->last().no) return frames->last().value; - auto frame = frames->data + bsearch(frames, frameNo); - if (frame->no == frameNo) return frame->value; + auto frame = frames->data + _bsearch(frames, frameNo); + if (mathEqual(frame->no, frameNo)) return frame->value; return frame->interpolate(frame + 1, frameNo); } + T operator()(float frameNo, LottieExpressions* exps) + { + if (exps && (exp && exp->enabled)) { + T out{}; + if (exp->loop.mode != LottieExpression::LoopMode::None) frameNo = _loop(frames, frameNo, exp); + if (exps->result>(frameNo, out, exp)) return out; + } + return operator()(frameNo); + } + T& operator=(const T& other) { //shallow copy, used for slot overriding - delete(frames); if (other.frames) { frames = other.frames; const_cast(other).frames = nullptr; @@ -275,10 +476,21 @@ struct LottiePathSet : LottieProperty ~LottiePathSet() { + release(); + } + + void release() + { + if (exp) { + delete(exp); + exp = nullptr; + } + free(value.cmds); free(value.pts); if (!frames) return; + for (auto p = frames->begin(); p < frames->end(); ++p) { free((*p).value.cmds); free((*p).value.pts); @@ -287,6 +499,21 @@ struct LottiePathSet : LottieProperty free(frames); } + uint32_t nearest(float frameNo) override + { + return _nearest(frames, frameNo); + } + + uint32_t frameCnt() override + { + return frames ? frames->count : 1; + } + + float frameNo(int32_t key) override + { + return _frameNo(frames, key); + } + LottieScalarFrame& newFrame() { if (!frames) { @@ -306,55 +533,68 @@ struct LottiePathSet : LottieProperty return (*frames)[frames->count]; } - bool operator()(float frameNo, Array& cmds, Array& pts) + bool operator()(float frameNo, Array& cmds, Array& pts, Matrix* transform, float roundness) { - if (!frames) { - copy(value, cmds); - copy(value, pts); - return true; + PathSet* path = nullptr; + LottieScalarFrame* frame = nullptr; + float t; + bool interpolate = false; + + if (!frames) path = &value; + else if (frames->count == 1 || frameNo <= frames->first().no) path = &frames->first().value; + else if (frameNo >= frames->last().no) path = &frames->last().value; + else { + frame = frames->data + _bsearch(frames, frameNo); + if (mathEqual(frame->no, frameNo)) path = &frame->value; + else { + t = (frameNo - frame->no) / ((frame + 1)->no - frame->no); + if (frame->interpolator) t = frame->interpolator->progress(t); + if (frame->hold) path = &(frame + ((t < 1.0f) ? 0 : 1))->value; + else interpolate = true; + } } - if (frames->count == 1 || frameNo <= frames->first().no) { - copy(frames->first().value, cmds); - copy(frames->first().value, pts); - return true; - } - - if (frameNo >= frames->last().no) { - copy(frames->last().value, cmds); - copy(frames->last().value, pts); - return true; - } - - auto frame = frames->data + bsearch(frames, frameNo); - - if (frame->no == frameNo) { - copy(frame->value, cmds); - copy(frame->value, pts); - return true; - } - - //interpolate - copy(frame->value, cmds); - - auto t = (frameNo - frame->no) / ((frame + 1)->no - frame->no); - if (frame->interpolator) t = frame->interpolator->progress(t); - - if (frame->hold) { - if (t < 1.0f) copy(frame->value, pts); - else copy((frame + 1)->value, pts); + if (!interpolate) { + if (roundness > ROUNDNESS_EPSILON) return _modifier(path->pts, path->ptsCnt, path->cmds, path->cmdsCnt, cmds, pts, transform, roundness); + _copy(path, cmds); + _copy(path, pts, transform); return true; } auto s = frame->value.pts; auto e = (frame + 1)->value.pts; - for (auto i = 0; i < frame->value.ptsCnt; ++i, ++s, ++e) { - pts.push(mathLerp(*s, *e, t)); + if (roundness > ROUNDNESS_EPSILON) { + auto interpPts = (Point*)malloc(frame->value.ptsCnt * sizeof(Point)); + auto p = interpPts; + for (auto i = 0; i < frame->value.ptsCnt; ++i, ++s, ++e, ++p) { + *p = mathLerp(*s, *e, t); + if (transform) mathMultiply(p, transform); + } + _modifier(interpPts, frame->value.ptsCnt, frame->value.cmds, frame->value.cmdsCnt, cmds, pts, nullptr, roundness); + free(interpPts); + return true; + } else { + for (auto i = 0; i < frame->value.ptsCnt; ++i, ++s, ++e) { + auto pt = mathLerp(*s, *e, t); + if (transform) mathMultiply(&pt, transform); + pts.push(pt); + } + _copy(&frame->value, cmds); } return true; } + + bool operator()(float frameNo, Array& cmds, Array& pts, Matrix* transform, float roundness, LottieExpressions* exps) + { + if (exps && (exp && exp->enabled)) { + if (exp->loop.mode != LottieExpression::LoopMode::None) frameNo = _loop(frames, frameNo, exp); + if (exps->result(frameNo, cmds, pts, transform, roundness, exp)) return true; + } + return operator()(frameNo, cmds, pts, transform, roundness); + } + void prepare() {} }; @@ -364,6 +604,7 @@ struct LottieColorStop : LottieProperty Array>* frames = nullptr; ColorStop value; uint16_t count = 0; //colorstop count + bool populated = false; ~LottieColorStop() { @@ -372,6 +613,11 @@ struct LottieColorStop : LottieProperty void release() { + if (exp) { + delete(exp); + exp = nullptr; + } + if (value.data) { free(value.data); value.data = nullptr; @@ -387,6 +633,21 @@ struct LottieColorStop : LottieProperty frames = nullptr; } + uint32_t nearest(float frameNo) override + { + return _nearest(frames, frameNo); + } + + uint32_t frameCnt() override + { + return frames ? frames->count : 1; + } + + float frameNo(int32_t key) override + { + return _frameNo(frames, key); + } + LottieScalarFrame& newFrame() { if (!frames) { @@ -406,28 +667,23 @@ struct LottieColorStop : LottieProperty return (*frames)[frames->count]; } - void operator()(float frameNo, Fill* fill) + Result operator()(float frameNo, Fill* fill, LottieExpressions* exps) { - if (!frames) { - fill->colorStops(value.data, count); - return; + if (exps && (exp && exp->enabled)) { + if (exp->loop.mode != LottieExpression::LoopMode::None) frameNo = _loop(frames, frameNo, exp); + if (exps->result(frameNo, fill, exp)) return Result::Success; } + if (!frames) return fill->colorStops(value.data, count); + if (frames->count == 1 || frameNo <= frames->first().no) { - fill->colorStops(frames->first().value.data, count); - return; + return fill->colorStops(frames->first().value.data, count); } - if (frameNo >= frames->last().no) { - fill->colorStops(frames->last().value.data, count); - return; - } + if (frameNo >= frames->last().no) return fill->colorStops(frames->last().value.data, count); - auto frame = frames->data + bsearch(frames, frameNo); - if (frame->no == frameNo) { - fill->colorStops(frame->value.data, count); - return; - } + auto frame = frames->data + _bsearch(frames, frameNo); + if (mathEqual(frame->no, frameNo)) return fill->colorStops(frame->value.data, count); //interpolate auto t = (frameNo - frame->no) / ((frame + 1)->no - frame->no); @@ -451,21 +707,22 @@ struct LottieColorStop : LottieProperty auto a = mathLerp(s->a, e->a, t); result.push({offset, r, g, b, a}); } - fill->colorStops(result.data, count); + return fill->colorStops(result.data, count); } LottieColorStop& operator=(const LottieColorStop& other) { //shallow copy, used for slot overriding - release(); if (other.frames) { frames = other.frames; const_cast(other).frames = nullptr; } else { value = other.value; - const_cast(other).value.data = nullptr; + const_cast(other).value = {nullptr, nullptr}; } + populated = other.populated; count = other.count; + return *this; } @@ -483,8 +740,34 @@ struct LottiePosition : LottieProperty } ~LottiePosition() + { + release(); + } + + void release() { delete(frames); + frames = nullptr; + + if (exp) { + delete(exp); + exp = nullptr; + } + } + + uint32_t nearest(float frameNo) override + { + return _nearest(frames, frameNo); + } + + uint32_t frameCnt() override + { + return frames ? frames->count : 1; + } + + float frameNo(int32_t key) override + { + return _frameNo(frames, key); } LottieVectorFrame& newFrame() @@ -510,18 +793,28 @@ struct LottiePosition : LottieProperty if (frames->count == 1 || frameNo <= frames->first().no) return frames->first().value; if (frameNo >= frames->last().no) return frames->last().value; - auto frame = frames->data + bsearch(frames, frameNo); - if (frame->no == frameNo) return frame->value; + auto frame = frames->data + _bsearch(frames, frameNo); + if (mathEqual(frame->no, frameNo)) return frame->value; return frame->interpolate(frame + 1, frameNo); } + Point operator()(float frameNo, LottieExpressions* exps) + { + Point out{}; + if (exps && (exp && exp->enabled)) { + if (exp->loop.mode != LottieExpression::LoopMode::None) frameNo = _loop(frames, frameNo, exp); + if (exps->result(frameNo, out, exp)) return out; + } + return operator()(frameNo); + } + float angle(float frameNo) { if (!frames) return 0; if (frames->count == 1 || frameNo <= frames->first().no) return 0; if (frameNo >= frames->last().no) return 0; - auto frame = frames->data + bsearch(frames, frameNo); + auto frame = frames->data + _bsearch(frames, frameNo); return frame->angle(frame + 1, frameNo); } @@ -547,6 +840,11 @@ struct LottieTextDoc : LottieProperty void release() { + if (exp) { + delete(exp); + exp = nullptr; + } + if (value.text) { free(value.text); value.text = nullptr; @@ -566,6 +864,21 @@ struct LottieTextDoc : LottieProperty frames = nullptr; } + uint32_t nearest(float frameNo) override + { + return _nearest(frames, frameNo); + } + + uint32_t frameCnt() override + { + return frames ? frames->count : 1; + } + + float frameNo(int32_t key) override + { + return _frameNo(frames, key); + } + LottieScalarFrame& newFrame() { if (!frames) frames = new Array>; @@ -589,14 +902,13 @@ struct LottieTextDoc : LottieProperty if (frames->count == 1 || frameNo <= frames->first().no) return frames->first().value; if (frameNo >= frames->last().no) return frames->last().value; - auto frame = frames->data + bsearch(frames, frameNo); + auto frame = frames->data + _bsearch(frames, frameNo); return frame->value; } LottieTextDoc& operator=(const LottieTextDoc& other) { //shallow copy, used for slot overriding - release(); if (other.frames) { frames = other.frames; const_cast(other).frames = nullptr; diff --git a/src/libs/thorvg/tvgMath.cpp b/src/libs/thorvg/tvgMath.cpp index 57c9d0a95..509a459d5 100644 --- a/src/libs/thorvg/tvgMath.cpp +++ b/src/libs/thorvg/tvgMath.cpp @@ -74,7 +74,7 @@ void mathRotate(Matrix* m, float degree) { if (degree == 0.0f) return; - auto radian = degree / 180.0f * M_PI; + auto radian = degree / 180.0f * MATH_PI; auto cosVal = cosf(radian); auto sinVal = sinf(radian); diff --git a/src/libs/thorvg/tvgMath.h b/src/libs/thorvg/tvgMath.h index 3836218e5..49bbe1864 100644 --- a/src/libs/thorvg/tvgMath.h +++ b/src/libs/thorvg/tvgMath.h @@ -34,6 +34,7 @@ #define MATH_PI 3.14159265358979323846f #define MATH_PI2 1.57079632679489661923f +#define FLOAT_EPSILON 1.0e-06f //1.192092896e-07f #define PATH_KAPPA 0.552284f #define mathMin(x, y) (((x) < (y)) ? (x) : (y)) @@ -47,15 +48,33 @@ bool mathIdentity(const Matrix* m); void mathMultiply(Point* pt, const Matrix* transform); +static inline float mathDeg2Rad(float degree) +{ + return degree * (MATH_PI / 180.0f); +} + + +static inline float mathRad2Deg(float radian) +{ + return radian * (180.0f / MATH_PI); +} + + static inline bool mathZero(float a) { - return (fabsf(a) < FLT_EPSILON) ? true : false; + return (fabsf(a) <= FLOAT_EPSILON) ? true : false; +} + + +static inline bool mathZero(const Point& p) +{ + return mathZero(p.x) && mathZero(p.y); } static inline bool mathEqual(float a, float b) { - return (fabsf(a - b) < FLT_EPSILON); + return mathZero(a - b); } @@ -73,14 +92,14 @@ static inline bool mathEqual(const Matrix& a, const Matrix& b) static inline bool mathRightAngle(const Matrix* m) { auto radian = fabsf(atan2f(m->e21, m->e11)); - if (radian < FLT_EPSILON || mathEqual(radian, float(M_PI_2)) || mathEqual(radian, float(M_PI))) return true; + if (radian < FLOAT_EPSILON || mathEqual(radian, MATH_PI2) || mathEqual(radian, MATH_PI)) return true; return false; } static inline bool mathSkewed(const Matrix* m) { - return (fabsf(m->e21 + m->e12) > FLT_EPSILON); + return !mathZero(m->e21 + m->e12); } @@ -160,6 +179,12 @@ static inline float mathLength(const Point* a, const Point* b) } +static inline float mathLength(const Point& a) +{ + return sqrtf(a.x * a.x + a.y * a.y); +} + + static inline Point operator-(const Point& lhs, const Point& rhs) { return {lhs.x - rhs.x, lhs.y - rhs.y}; diff --git a/src/libs/thorvg/tvgPaint.cpp b/src/libs/thorvg/tvgPaint.cpp index dc835ba7c..4db483107 100644 --- a/src/libs/thorvg/tvgPaint.cpp +++ b/src/libs/thorvg/tvgPaint.cpp @@ -44,20 +44,26 @@ } -static bool _compFastTrack(Paint* cmpTarget, const RenderTransform* pTransform, RenderTransform* rTransform, RenderRegion& viewport) + +static Result _compFastTrack(Paint* cmpTarget, const RenderTransform* pTransform, RenderTransform* rTransform, RenderRegion& viewport) { /* Access Shape class by Paint is bad... but it's ok still it's an internal usage. */ auto shape = static_cast(cmpTarget); //Rectangle Candidates? const Point* pts; - if (shape->pathCoords(&pts) != 4) return false; + auto ptsCnt = shape->pathCoords(&pts); + + //nothing to clip + if (ptsCnt == 0) return Result::InvalidArguments; + + if (ptsCnt != 4) return Result::InsufficientCondition; if (rTransform) rTransform->update(); //No rotation and no skewing - if (pTransform && (!mathRightAngle(&pTransform->m) || mathSkewed(&pTransform->m))) return false; - if (rTransform && (!mathRightAngle(&rTransform->m) || mathSkewed(&rTransform->m))) return false; + if (pTransform && (!mathRightAngle(&pTransform->m) || mathSkewed(&pTransform->m))) return Result::InsufficientCondition; + if (rTransform && (!mathRightAngle(&rTransform->m) || mathSkewed(&rTransform->m))) return Result::InsufficientCondition; //Perpendicular Rectangle? auto pt1 = pts + 0; @@ -102,10 +108,9 @@ static bool _compFastTrack(Paint* cmpTarget, const RenderTransform* pTransform, if (viewport.w < 0) viewport.w = 0; if (viewport.h < 0) viewport.h = 0; - return true; + return Result::Success; } - - return false; + return Result::InsufficientCondition; } @@ -238,7 +243,7 @@ RenderData Paint::Impl::update(RenderMethod* renderer, const RenderTransform* pT /* 1. Composition Pre Processing */ RenderData trd = nullptr; //composite target render data RenderRegion viewport; - bool compFastTrack = false; + Result compFastTrack = Result::InsufficientCondition; bool childClipper = false; if (compData) { @@ -263,7 +268,7 @@ RenderData Paint::Impl::update(RenderMethod* renderer, const RenderTransform* pT } if (tryFastTrack) { RenderRegion viewport2; - if ((compFastTrack = _compFastTrack(target, pTransform, target->pImpl->rTransform, viewport2))) { + if ((compFastTrack = _compFastTrack(target, pTransform, target->pImpl->rTransform, viewport2)) == Result::Success) { viewport = renderer->viewport(); viewport2.intersect(viewport); renderer->viewport(viewport2); @@ -271,7 +276,7 @@ RenderData Paint::Impl::update(RenderMethod* renderer, const RenderTransform* pT } } } - if (!compFastTrack) { + if (compFastTrack == Result::InsufficientCondition) { childClipper = compData->method == CompositeMethod::ClipPath ? true : false; trd = target->pImpl->update(renderer, pTransform, clips, 255, pFlag, childClipper); if (childClipper) clips.push(trd); @@ -288,7 +293,7 @@ RenderData Paint::Impl::update(RenderMethod* renderer, const RenderTransform* pT PAINT_METHOD(rd, update(renderer, &outTransform, clips, opacity, newFlag, clipper)); /* 3. Composition Post Processing */ - if (compFastTrack) renderer->viewport(viewport); + if (compFastTrack == Result::Success) renderer->viewport(viewport); else if (childClipper) clips.pop(); return rd; diff --git a/src/libs/thorvg/tvgPicture.cpp b/src/libs/thorvg/tvgPicture.cpp index 5c05ee6df..3d7859d22 100644 --- a/src/libs/thorvg/tvgPicture.cpp +++ b/src/libs/thorvg/tvgPicture.cpp @@ -32,7 +32,9 @@ RenderUpdateFlag Picture::Impl::load() { if (loader) { - if (!paint) { + if (paint) { + loader->sync(); + } else { paint = loader->paint(); if (paint) { if (w != loader->w || h != loader->h) { @@ -45,8 +47,7 @@ RenderUpdateFlag Picture::Impl::load() } return RenderUpdateFlag::None; } - } else loader->sync(); - + } if (!surface) { if ((surface = loader->bitmap())) { return RenderUpdateFlag::Image; @@ -136,7 +137,7 @@ Result Picture::Impl::load(ImageLoader* loader) if (!loader->read()) return Result::Unknown; this->w = loader->w; - this->h = loader->h; + this->h = loader->h; return Result::Success; } diff --git a/src/libs/thorvg/tvgRender.cpp b/src/libs/thorvg/tvgRender.cpp index 48b78200d..dad20e8b5 100644 --- a/src/libs/thorvg/tvgRender.cpp +++ b/src/libs/thorvg/tvgRender.cpp @@ -64,5 +64,37 @@ RenderTransform::RenderTransform(const RenderTransform* lhs, const RenderTransfo else mathIdentity(&m); } + +void RenderRegion::intersect(const RenderRegion& rhs) +{ + auto x1 = x + w; + auto y1 = y + h; + auto x2 = rhs.x + rhs.w; + auto y2 = rhs.y + rhs.h; + + x = (x > rhs.x) ? x : rhs.x; + y = (y > rhs.y) ? y : rhs.y; + w = ((x1 < x2) ? x1 : x2) - x; + h = ((y1 < y2) ? y1 : y2) - y; + + if (w < 0) w = 0; + if (h < 0) h = 0; +} + + +void RenderRegion::add(const RenderRegion& rhs) +{ + if (rhs.x < x) { + w += (x - rhs.x); + x = rhs.x; + } + if (rhs.y < y) { + h += (y - rhs.y); + y = rhs.y; + } + if (rhs.x + rhs.w > x + w) w = (rhs.x + rhs.w) - x; + if (rhs.y + rhs.h > y + h) h = (rhs.y + rhs.h) - y; +} + #endif /* LV_USE_THORVG_INTERNAL */ diff --git a/src/libs/thorvg/tvgRender.h b/src/libs/thorvg/tvgRender.h index 999a03178..8467a9101 100644 --- a/src/libs/thorvg/tvgRender.h +++ b/src/libs/thorvg/tvgRender.h @@ -103,34 +103,13 @@ struct RenderRegion { int32_t x, y, w, h; - void intersect(const RenderRegion& rhs) + void intersect(const RenderRegion& rhs); + void add(const RenderRegion& rhs); + + bool operator==(const RenderRegion& rhs) { - auto x1 = x + w; - auto y1 = y + h; - auto x2 = rhs.x + rhs.w; - auto y2 = rhs.y + rhs.h; - - x = (x > rhs.x) ? x : rhs.x; - y = (y > rhs.y) ? y : rhs.y; - w = ((x1 < x2) ? x1 : x2) - x; - h = ((y1 < y2) ? y1 : y2) - y; - - if (w < 0) w = 0; - if (h < 0) h = 0; - } - - void add(const RenderRegion& rhs) - { - if (rhs.x < x) { - w += (x - rhs.x); - x = rhs.x; - } - if (rhs.y < y) { - h += (y - rhs.y); - y = rhs.y; - } - if (rhs.x + rhs.w > x + w) w = (rhs.x + rhs.w) - x; - if (rhs.y + rhs.h > y + h) h = (rhs.y + rhs.h) - y; + if (x == rhs.x && y == rhs.y && w == rhs.w && h == rhs.h) return true; + return false; } }; @@ -166,6 +145,7 @@ struct RenderStroke struct { float begin = 0.0f; float end = 1.0f; + bool individual = false; } trim; ~RenderStroke() @@ -295,6 +275,7 @@ public: virtual bool viewport(const RenderRegion& vp) = 0; virtual bool blend(BlendMethod method) = 0; virtual ColorSpace colorSpace() = 0; + virtual const Surface* mainSurface() = 0; virtual bool clear() = 0; virtual bool sync() = 0; diff --git a/src/libs/thorvg/tvgSaver.cpp b/src/libs/thorvg/tvgSaver.cpp index 952f73c0a..620a25162 100644 --- a/src/libs/thorvg/tvgSaver.cpp +++ b/src/libs/thorvg/tvgSaver.cpp @@ -160,14 +160,17 @@ Result Saver::save(unique_ptr animation, const string& path, uint32_t auto a = animation.release(); if (!a) return Result::MemoryCorruption; + //animation holds the picture, it must be 1 at the bottom. + auto remove = PP(a->picture())->refCnt <= 1 ? true : false; + if (mathZero(a->totalFrame())) { - delete(a); + if (remove) delete(a); return Result::InsufficientCondition; } //Already on saving an other resource. if (pImpl->saveModule) { - delete(a); + if (remove) delete(a); return Result::InsufficientCondition; } @@ -176,12 +179,12 @@ Result Saver::save(unique_ptr animation, const string& path, uint32_t pImpl->saveModule = saveModule; return Result::Success; } else { - delete(a); + if (remove) delete(a); delete(saveModule); return Result::Unknown; } } - delete(a); + if (remove) delete(a); return Result::NonSupport; } diff --git a/src/libs/thorvg/tvgScene.h b/src/libs/thorvg/tvgScene.h index 2d74a56a5..41c0156e9 100644 --- a/src/libs/thorvg/tvgScene.h +++ b/src/libs/thorvg/tvgScene.h @@ -136,7 +136,6 @@ struct Scene::Impl if (needComp) { cmp = renderer->target(bounds(renderer), renderer->colorSpace()); renderer->beginComposite(cmp, CompositeMethod::None, opacity); - needComp = false; } for (auto paint : paints) { diff --git a/src/libs/thorvg/tvgShape.cpp b/src/libs/thorvg/tvgShape.cpp index fdfe202a3..fcf77e242 100644 --- a/src/libs/thorvg/tvgShape.cpp +++ b/src/libs/thorvg/tvgShape.cpp @@ -148,13 +148,15 @@ Result Shape::appendArc(float cx, float cy, float radius, float startAngle, floa //just circle if (sweep >= 360.0f || sweep <= -360.0f) return appendCircle(cx, cy, radius, radius); - startAngle = (startAngle * MATH_PI) / 180.0f; - sweep = sweep * MATH_PI / 180.0f; + const float arcPrecision = 1e-5f; + startAngle = mathDeg2Rad(startAngle); + sweep = mathDeg2Rad(sweep); - auto nCurves = ceil(fabsf(sweep / MATH_PI2)); + auto nCurves = static_cast(fabsf(sweep / MATH_PI2)); + if (fabsf(sweep / MATH_PI2) - nCurves > arcPrecision) ++nCurves; auto sweepSign = (sweep < 0 ? -1 : 1); auto fract = fmodf(sweep, MATH_PI2); - fract = (mathZero(fract)) ? MATH_PI2 * sweepSign : fract; + fract = (fabsf(fract) < arcPrecision) ? MATH_PI2 * sweepSign : fract; //Start from here Point start = {radius * cosf(startAngle), radius * sinf(startAngle)}; @@ -167,7 +169,7 @@ Result Shape::appendArc(float cx, float cy, float radius, float startAngle, floa } for (int i = 0; i < nCurves; ++i) { - auto endAngle = startAngle + ((i != nCurves - 1) ? float(M_PI_2) * sweepSign : fract); + auto endAngle = startAngle + ((i != nCurves - 1) ? MATH_PI2 * sweepSign : fract); Point end = {radius * cosf(endAngle), radius * sinf(endAngle)}; //variables needed to calculate bezier control points diff --git a/src/libs/thorvg/tvgShape.h b/src/libs/thorvg/tvgShape.h index ebd4bd8e9..9fcea0e0e 100644 --- a/src/libs/thorvg/tvgShape.h +++ b/src/libs/thorvg/tvgShape.h @@ -59,7 +59,6 @@ struct Shape::Impl if (needComp) { cmp = renderer->target(bounds(renderer), renderer->colorSpace()); renderer->beginComposite(cmp, CompositeMethod::None, opacity); - needComp = false; } ret = renderer->renderShape(rd); if (cmp) renderer->endComposite(cmp); @@ -71,7 +70,7 @@ struct Shape::Impl if (opacity == 0) return false; //Shape composition is only necessary when stroking & fill are valid. - if (!rs.stroke || rs.stroke->width < FLT_EPSILON || (!rs.stroke->fill && rs.stroke->color[3] == 0)) return false; + if (!rs.stroke || rs.stroke->width < FLOAT_EPSILON || (!rs.stroke->fill && rs.stroke->color[3] == 0)) return false; if (!rs.fill && rs.color[3] == 0) return false; //translucent fill & stroke @@ -80,17 +79,30 @@ struct Shape::Impl //Composition test const Paint* target; auto method = shape->composite(&target); - if (!target || method == tvg::CompositeMethod::ClipPath) return false; - if (target->pImpl->opacity == 255 || target->pImpl->opacity == 0) return false; + if (!target || method == CompositeMethod::ClipPath) return false; + if (target->pImpl->opacity == 255 || target->pImpl->opacity == 0) { + if (target->identifier() == TVG_CLASS_ID_SHAPE) { + auto shape = static_cast(target); + if (!shape->fill()) { + uint8_t r, g, b, a; + shape->fillColor(&r, &g, &b, &a); + if (a == 0 || a == 255) { + if (method == CompositeMethod::LumaMask || method == CompositeMethod::InvLumaMask) { + if ((r == 255 && g == 255 && b == 255) || (r == 0 && g == 0 && b == 0)) return false; + } else return false; + } + } + } + } return true; } RenderData update(RenderMethod* renderer, const RenderTransform* transform, Array& clips, uint8_t opacity, RenderUpdateFlag pFlag, bool clipper) - { + { if ((needComp = needComposition(opacity))) { /* Overriding opacity value. If this scene is half-translucent, - It must do intermeidate composition with that opacity value. */ + It must do intermeidate composition with that opacity value. */ this->opacity = opacity; opacity = 255; } @@ -207,7 +219,7 @@ struct Shape::Impl return true; } - bool strokeTrim(float begin, float end) + bool strokeTrim(float begin, float end, bool individual) { if (!rs.stroke) { if (begin == 0.0f && end == 1.0f) return true; @@ -218,6 +230,7 @@ struct Shape::Impl rs.stroke->trim.begin = begin; rs.stroke->trim.end = end; + rs.stroke->trim.individual = individual; flag |= RenderUpdateFlag::Stroke; return true; @@ -291,7 +304,7 @@ struct Shape::Impl } for (uint32_t i = 0; i < cnt; i++) { - if (pattern[i] < FLT_EPSILON) return Result::InvalidArguments; + if (pattern[i] < FLOAT_EPSILON) return Result::InvalidArguments; } //Reset dash diff --git a/src/libs/thorvg/tvgStr.cpp b/src/libs/thorvg/tvgStr.cpp index cbaf42fe2..f0ece4c3f 100644 --- a/src/libs/thorvg/tvgStr.cpp +++ b/src/libs/thorvg/tvgStr.cpp @@ -24,6 +24,7 @@ #if LV_USE_THORVG_INTERNAL #include "config.h" +#include #include #include #include "tvgMath.h" @@ -200,6 +201,8 @@ float strToFloat(const char *nPtr, char **endPtr) success: if (endPtr) *endPtr = (char *)(a); + if (!std::isfinite(val)) return 0.0f; + return minus * val; error: diff --git a/src/libs/thorvg/tvgSvgCssStyle.cpp b/src/libs/thorvg/tvgSvgCssStyle.cpp index 245043e89..11274e332 100644 --- a/src/libs/thorvg/tvgSvgCssStyle.cpp +++ b/src/libs/thorvg/tvgSvgCssStyle.cpp @@ -52,6 +52,22 @@ static void _copyStyle(SvgStyleProperty* to, const SvgStyleProperty* from) to->flagsImportance = (to->flagsImportance | SvgStyleFlags::Color); } } + if (((from->flags & SvgStyleFlags::PaintOrder) && !(to->flags & SvgStyleFlags::PaintOrder)) || + _isImportanceApplicable(to->flagsImportance, from->flagsImportance, SvgStyleFlags::PaintOrder)) { + to->paintOrder = from->paintOrder; + to->flags = (to->flags | SvgStyleFlags::PaintOrder); + if (from->flagsImportance & SvgStyleFlags::PaintOrder) { + to->flagsImportance = (to->flagsImportance | SvgStyleFlags::PaintOrder); + } + } + if (((from->flags & SvgStyleFlags::Display) && !(to->flags & SvgStyleFlags::Display)) || + _isImportanceApplicable(to->flagsImportance, from->flagsImportance, SvgStyleFlags::Display)) { + to->display = from->display; + to->flags = (to->flags | SvgStyleFlags::Display); + if (from->flagsImportance & SvgStyleFlags::Display) { + to->flagsImportance = (to->flagsImportance | SvgStyleFlags::Display); + } + } //Fill if (((from->fill.flags & SvgFillFlags::Paint) && !(to->flags & SvgStyleFlags::Fill)) || _isImportanceApplicable(to->flagsImportance, from->flagsImportance, SvgStyleFlags::Fill)) { diff --git a/src/libs/thorvg/tvgSvgLoader.cpp b/src/libs/thorvg/tvgSvgLoader.cpp index 0e9642855..44677216b 100644 --- a/src/libs/thorvg/tvgSvgLoader.cpp +++ b/src/libs/thorvg/tvgSvgLoader.cpp @@ -106,15 +106,19 @@ static const char* _skipComma(const char* content) } -static bool _parseNumber(const char** content, float* number) +static bool _parseNumber(const char** content, const char** end, float* number) { - char* end = nullptr; + const char* _end = end ? *end : nullptr; - *number = strToFloat(*content, &end); + *number = strToFloat(*content, (char**)&_end); //If the start of string is not number - if ((*content) == end) return false; + if ((*content) == _end) { + if (end) *end = _end; + return false; + } //Skip comma if any - *content = _skipComma(end); + *content = _skipComma(_end); + if (end) *end = _end; return true; } @@ -579,7 +583,82 @@ static constexpr struct }; -static void _toColor(const char* str, uint8_t* r, uint8_t* g, uint8_t* b, char** ref) +static bool _hslToRgb(float hue, float satuation, float brightness, uint8_t* red, uint8_t* green, uint8_t* blue) +{ + if (!red || !green || !blue) return false; + + float sv, vsf, f, p, q, t, v; + float _red = 0, _green = 0, _blue = 0; + uint32_t i = 0; + + if (mathZero(satuation)) _red = _green = _blue = brightness; + else { + if (mathEqual(hue, 360.0)) hue = 0.0f; + hue /= 60.0f; + + v = (brightness <= 0.5f) ? (brightness * (1.0f + satuation)) : (brightness + satuation - (brightness * satuation)); + p = brightness + brightness - v; + + if (!mathZero(v)) sv = (v - p) / v; + else sv = 0; + + i = static_cast(hue); + f = hue - i; + + vsf = v * sv * f; + + t = p + vsf; + q = v - vsf; + + switch (i) { + case 0: { + _red = v; + _green = t; + _blue = p; + break; + } + case 1: { + _red = q; + _green = v; + _blue = p; + break; + } + case 2: { + _red = p; + _green = v; + _blue = t; + break; + } + case 3: { + _red = p; + _green = q; + _blue = v; + break; + } + case 4: { + _red = t; + _green = p; + _blue = v; + break; + } + case 5: { + _red = v; + _green = p; + _blue = q; + break; + } + } + } + + *red = static_cast(roundf(_red * 255.0f)); + *green = static_cast(roundf(_green * 255.0f)); + *blue = static_cast(roundf(_blue * 255.0f)); + + return true; +} + + +static bool _toColor(const char* str, uint8_t* r, uint8_t* g, uint8_t* b, char** ref) { unsigned int len = strlen(str); char *red, *green, *blue; @@ -599,6 +678,7 @@ static void _toColor(const char* str, uint8_t* r, uint8_t* g, uint8_t* b, char** tmp[1] = str[3]; *b = strtol(tmp, nullptr, 16); } + return true; } else if (len == 7 && str[0] == '#') { if (isxdigit(str[1]) && isxdigit(str[2]) && isxdigit(str[3]) && isxdigit(str[4]) && isxdigit(str[5]) && isxdigit(str[6])) { char tmp[3] = { '\0', '\0', '\0' }; @@ -612,6 +692,7 @@ static void _toColor(const char* str, uint8_t* r, uint8_t* g, uint8_t* b, char** tmp[1] = str[6]; *b = strtol(tmp, nullptr, 16); } + return true; } else if (len >= 10 && (str[0] == 'r' || str[0] == 'R') && (str[1] == 'g' || str[1] == 'G') && (str[2] == 'b' || str[2] == 'B') && str[3] == '(' && str[len - 1] == ')') { tr = _parseColor(str + 4, &red); if (red && *red == ',') { @@ -625,9 +706,35 @@ static void _toColor(const char* str, uint8_t* r, uint8_t* g, uint8_t* b, char** } } } + return true; } else if (ref && len >= 3 && !strncmp(str, "url", 3)) { if (*ref) free(*ref); *ref = _idFromUrl((const char*)(str + 3)); + return true; + } else if (len >= 10 && (str[0] == 'h' || str[0] == 'H') && (str[1] == 's' || str[1] == 'S') && (str[2] == 'l' || str[2] == 'L') && str[3] == '(' && str[len - 1] == ')') { + float th, ts, tb; + const char *content, *hue, *satuation, *brightness; + content = str + 4; + content = _skipSpace(content, nullptr); + if (_parseNumber(&content, &hue, &th) && hue) { + th = float(uint32_t(th) % 360); + hue = _skipSpace(hue, nullptr); + hue = (char*)_skipComma(hue); + hue = _skipSpace(hue, nullptr); + if (_parseNumber(&hue, &satuation, &ts) && satuation && *satuation == '%') { + ts /= 100.0f; + satuation = _skipSpace(satuation + 1, nullptr); + satuation = (char*)_skipComma(satuation); + satuation = _skipSpace(satuation, nullptr); + if (_parseNumber(&satuation, &brightness, &tb) && brightness && *brightness == '%') { + tb /= 100.0f; + brightness = _skipSpace(brightness + 1, nullptr); + if (brightness && brightness[0] == ')' && brightness[1] == '\0') { + return _hslToRgb(th, ts, tb, r, g, b); + } + } + } + } } else { //Handle named color for (unsigned int i = 0; i < (sizeof(colors) / sizeof(colors[0])); i++) { @@ -635,10 +742,11 @@ static void _toColor(const char* str, uint8_t* r, uint8_t* g, uint8_t* b, char** *r = (((uint8_t*)(&(colors[i].value)))[2]); *g = (((uint8_t*)(&(colors[i].value)))[1]); *b = (((uint8_t*)(&(colors[i].value)))[0]); - return; + return true; } } } + return false; } @@ -746,10 +854,10 @@ static Matrix* _parseTransformationMatrix(const char* value) } else goto error; } else if (state == MatrixState::Rotate) { //Transform to signed. - points[0] = fmod(points[0], 360); - if (points[0] < 0) points[0] += 360; - auto c = cosf(points[0] * (M_PI / 180.0)); - auto s = sinf(points[0] * (M_PI / 180.0)); + points[0] = fmodf(points[0], 360.0f); + if (points[0] < 0) points[0] += 360.0f; + auto c = cosf(mathDeg2Rad(points[0])); + auto s = sinf(mathDeg2Rad(points[0])); if (ptCount == 1) { Matrix tmp = { c, -s, 0, s, c, 0, 0, 0, 1 }; *matrix = mathMultiply(matrix, &tmp); @@ -772,12 +880,12 @@ static Matrix* _parseTransformationMatrix(const char* value) *matrix = mathMultiply(matrix, &tmp); } else if (state == MatrixState::SkewX) { if (ptCount != 1) goto error; - auto deg = tanf(points[0] * (M_PI / 180.0)); + auto deg = tanf(mathDeg2Rad(points[0])); Matrix tmp = { 1, deg, 0, 0, 1, 0, 0, 0, 1 }; *matrix = mathMultiply(matrix, &tmp); } else if (state == MatrixState::SkewY) { if (ptCount != 1) goto error; - auto deg = tanf(points[0] * (M_PI / 180.0)); + auto deg = tanf(mathDeg2Rad(points[0])); Matrix tmp = { 1, 0, 0, deg, 1, 0, 0, 0, 1 }; *matrix = mathMultiply(matrix, &tmp); } @@ -857,10 +965,10 @@ static bool _attrParseSvgNode(void* data, const char* key, const char* value) doc->viewFlag = (doc->viewFlag | SvgViewFlag::Height); } } else if (!strcmp(key, "viewBox")) { - if (_parseNumber(&value, &doc->vx)) { - if (_parseNumber(&value, &doc->vy)) { - if (_parseNumber(&value, &doc->vw)) { - if (_parseNumber(&value, &doc->vh)) { + if (_parseNumber(&value, nullptr, &doc->vx)) { + if (_parseNumber(&value, nullptr, &doc->vy)) { + if (_parseNumber(&value, nullptr, &doc->vw)) { + if (_parseNumber(&value, nullptr, &doc->vh)) { doc->viewFlag = (doc->viewFlag | SvgViewFlag::Viewbox); loader->svgParse->global.h = doc->vh; } @@ -883,7 +991,7 @@ static bool _attrParseSvgNode(void* data, const char* key, const char* value) } else if (!strcmp(key, "style")) { return simpleXmlParseW3CAttribute(value, strlen(value), _parseStyleAttr, loader); #ifdef THORVG_LOG_ENABLED - } else if ((!strcmp(key, "x") || !strcmp(key, "y")) && fabsf(strToFloat(value, nullptr)) > FLT_EPSILON) { + } else if ((!strcmp(key, "x") || !strcmp(key, "y")) && fabsf(strToFloat(value, nullptr)) > FLOAT_EPSILON) { TVGLOG("SVG", "Unsupported attributes used [Elements type: Svg][Attribute: %s][Value: %s]", key, value); #endif } else { @@ -901,20 +1009,21 @@ static void _handlePaintAttr(SvgPaint* paint, const char* value) paint->none = true; return; } - paint->none = false; if (!strcmp(value, "currentColor")) { paint->curColor = true; + paint->none = false; return; } - _toColor(value, &paint->color.r, &paint->color.g, &paint->color.b, &paint->url); + if (_toColor(value, &paint->color.r, &paint->color.g, &paint->color.b, &paint->url)) paint->none = false; } static void _handleColorAttr(TVG_UNUSED SvgLoaderData* loader, SvgNode* node, const char* value) { SvgStyleProperty* style = node->style; - style->curColorSet = true; - _toColor(value, &style->color.r, &style->color.g, &style->color.b, nullptr); + if (_toColor(value, &style->color.r, &style->color.g, &style->color.b, nullptr)) { + style->curColorSet = true; + } } @@ -998,6 +1107,7 @@ static void _handleFillRuleAttr(TVG_UNUSED SvgLoaderData* loader, SvgNode* node, static void _handleOpacityAttr(TVG_UNUSED SvgLoaderData* loader, SvgNode* node, const char* value) { + node->style->flags = (node->style->flags | SvgStyleFlags::Opacity); node->style->opacity = _toOpacity(value); } @@ -1049,8 +1159,9 @@ static void _handleDisplayAttr(TVG_UNUSED SvgLoaderData* loader, SvgNode* node, // The default is "inline" which means visible and "none" means invisible. // Depending on the type of node, additional functionality may be required. // refer to https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/display - if (!strcmp(value, "none")) node->display = false; - else node->display = true; + node->style->flags = (node->style->flags | SvgStyleFlags::Display); + if (!strcmp(value, "none")) node->style->display = false; + else node->style->display = true; } @@ -1270,8 +1381,8 @@ static bool _attrParseSymbolNode(void* data, const char* key, const char* value) SvgSymbolNode* symbol = &(node->node.symbol); if (!strcmp(key, "viewBox")) { - if (!_parseNumber(&value, &symbol->vx) || !_parseNumber(&value, &symbol->vy)) return false; - if (!_parseNumber(&value, &symbol->vw) || !_parseNumber(&value, &symbol->vh)) return false; + if (!_parseNumber(&value, nullptr, &symbol->vx) || !_parseNumber(&value, nullptr, &symbol->vy)) return false; + if (!_parseNumber(&value, nullptr, &symbol->vw) || !_parseNumber(&value, nullptr, &symbol->vh)) return false; symbol->hasViewBox = true; } else if (!strcmp(key, "width")) { symbol->w = _toFloat(loader->svgParse, value, SvgParserLengthType::Horizontal); @@ -1335,7 +1446,7 @@ static SvgNode* _createNode(SvgNode* parent, SvgNodeType type) node->style->paintOrder = _toPaintOrder("fill stroke"); //Default display is true("inline"). - node->display = true; + node->style->display = true; node->parent = parent; node->type = type; @@ -1411,7 +1522,7 @@ static SvgNode* _createClipPathNode(SvgLoaderData* loader, SvgNode* parent, cons loader->svgParse->node = _createNode(parent, SvgNodeType::ClipPath); if (!loader->svgParse->node) return nullptr; - loader->svgParse->node->display = false; + loader->svgParse->node->style->display = false; loader->svgParse->node->node.clip.userSpace = true; func(buf, bufLength, _attrParseClipPathNode, loader); @@ -1436,7 +1547,6 @@ static SvgNode* _createSymbolNode(SvgLoaderData* loader, SvgNode* parent, const loader->svgParse->node = _createNode(parent, SvgNodeType::Symbol); if (!loader->svgParse->node) return nullptr; - loader->svgParse->node->display = false; loader->svgParse->node->node.symbol.align = AspectRatioAlign::XMidYMid; loader->svgParse->node->node.symbol.meetOrSlice = AspectRatioMeetOrSlice::Meet; loader->svgParse->node->node.symbol.overflowVisible = false; @@ -1618,8 +1728,11 @@ static SvgNode* _createEllipseNode(SvgLoaderData* loader, SvgNode* parent, const static bool _attrParsePolygonPoints(const char* str, SvgPolygonNode* polygon) { - float num; - while (_parseNumber(&str, &num)) polygon->pts.push(num); + float num_x, num_y; + while (_parseNumber(&str, nullptr, &num_x) && _parseNumber(&str, nullptr, &num_y)) { + polygon->pts.push(num_x); + polygon->pts.push(num_y); + } return true; } @@ -1714,8 +1827,8 @@ static bool _attrParseRectNode(void* data, const char* key, const char* value) if (!strncmp(rectTags[i].tag, "rx", sz)) rect->hasRx = true; if (!strncmp(rectTags[i].tag, "ry", sz)) rect->hasRy = true; - if ((rect->rx >= FLT_EPSILON) && (rect->ry < FLT_EPSILON) && rect->hasRx && !rect->hasRy) rect->ry = rect->rx; - if ((rect->ry >= FLT_EPSILON) && (rect->rx < FLT_EPSILON) && !rect->hasRx && rect->hasRy) rect->rx = rect->ry; + if ((rect->rx >= FLOAT_EPSILON) && (rect->ry < FLOAT_EPSILON) && rect->hasRx && !rect->hasRy) rect->ry = rect->rx; + if ((rect->ry >= FLOAT_EPSILON) && (rect->rx < FLOAT_EPSILON) && !rect->hasRx && rect->hasRy) rect->rx = rect->ry; return ret; } } @@ -1922,6 +2035,19 @@ static SvgNode* _findNodeById(SvgNode *node, const char* id) } +static SvgNode* _findParentById(SvgNode* node, char* id, SvgNode* doc) +{ + SvgNode *parent = node->parent; + while (parent != nullptr && parent != doc) { + if (parent->id && !strcmp(parent->id, id)) { + return parent; + } + parent = parent->parent; + } + return nullptr; +} + + static constexpr struct { const char* tag; @@ -1962,8 +2088,12 @@ static bool _attrParseUseNode(void* data, const char* key, const char* value) defs = _getDefsNode(node); nodeFrom = _findNodeById(defs, id); if (nodeFrom) { - _cloneNode(nodeFrom, node, 0); - if (nodeFrom->type == SvgNodeType::Symbol) use->symbol = nodeFrom; + if (!_findParentById(node, id, loader->doc)) { + _cloneNode(nodeFrom, node, 0); + if (nodeFrom->type == SvgNodeType::Symbol) use->symbol = nodeFrom; + } else { + TVGLOG("SVG", "%s is ancestor element. This reference is invalid.", id); + } free(id); } else { //some svg export software include element at the end of the file @@ -2364,8 +2494,9 @@ static bool _attrParseStopsStyle(void* data, const char* key, const char* value) stop->a = _toOpacity(value); loader->svgParse->flags = (loader->svgParse->flags | SvgStopStyleFlags::StopOpacity); } else if (!strcmp(key, "stop-color")) { - _toColor(value, &stop->r, &stop->g, &stop->b, nullptr); - loader->svgParse->flags = (loader->svgParse->flags | SvgStopStyleFlags::StopColor); + if (_toColor(value, &stop->r, &stop->g, &stop->b, nullptr)) { + loader->svgParse->flags = (loader->svgParse->flags | SvgStopStyleFlags::StopColor); + } } else { return false; } @@ -2672,7 +2803,7 @@ static void _inheritGradient(SvgLoaderData* loader, SvgStyleGradient* to, SvgSty if (to->transform) memcpy(to->transform, from->transform, sizeof(Matrix)); } - if (to->type == SvgGradientType::Linear && from->type == SvgGradientType::Linear) { + if (to->type == SvgGradientType::Linear) { for (unsigned int i = 0; i < sizeof(linear_tags) / sizeof(linear_tags[0]); i++) { bool coordSet = to->flags & linear_tags[i].flag; if (!(to->flags & linear_tags[i].flag) && (from->flags & linear_tags[i].flag)) { @@ -2689,7 +2820,7 @@ static void _inheritGradient(SvgLoaderData* loader, SvgStyleGradient* to, SvgSty linear_tags[i].tagInheritedRecalc(loader, to->linear, to->userSpace); } } - } else if (to->type == SvgGradientType::Radial && from->type == SvgGradientType::Radial) { + } else if (to->type == SvgGradientType::Radial) { for (unsigned int i = 0; i < sizeof(radialTags) / sizeof(radialTags[0]); i++) { bool coordSet = (to->flags & radialTags[i].flag); if (!(to->flags & radialTags[i].flag) && (from->flags & radialTags[i].flag)) { @@ -2699,10 +2830,16 @@ static void _inheritGradient(SvgLoaderData* loader, SvgStyleGradient* to, SvgSty //GradUnits not set directly, coord set if (!gradUnitSet && coordSet) { radialTags[i].tagRecalc(loader, to->radial, to->userSpace); + //If fx and fy are not set, set cx and cy. + if (!strcmp(radialTags[i].tag, "cx") && !(to->flags & SvgGradientFlags::Fx)) to->radial->fx = to->radial->cx; + if (!strcmp(radialTags[i].tag, "cy") && !(to->flags & SvgGradientFlags::Fy)) to->radial->fy = to->radial->cy; } //GradUnits set, coord not set directly if (to->userSpace == from->userSpace) continue; if (gradUnitSet && !coordSet) { + //If fx and fx are not set, do not call recalc. + if (!strcmp(radialTags[i].tag, "fx") && !(to->flags & SvgGradientFlags::Fx)) continue; + if (!strcmp(radialTags[i].tag, "fy") && !(to->flags & SvgGradientFlags::Fy)) continue; radialTags[i].tagInheritedRecalc(loader, to->radial, to->userSpace); } } @@ -2829,9 +2966,15 @@ static void _styleCopy(SvgStyleProperty* to, const SvgStyleProperty* from) to->color = from->color; to->curColorSet = true; } + if (from->flags & SvgStyleFlags::Opacity) { + to->opacity = from->opacity; + } if (from->flags & SvgStyleFlags::PaintOrder) { to->paintOrder = from->paintOrder; } + if (from->flags & SvgStyleFlags::Display) { + to->display = from->display; + } //Fill to->fill.flags = (to->fill.flags | from->fill.flags); if (from->fill.flags & SvgFillFlags::Paint) { @@ -3021,9 +3164,13 @@ static void _clonePostponedNodes(Array* cloneNodes, SvgNode* doc) auto defs = _getDefsNode(nodeIdPair.node); auto nodeFrom = _findNodeById(defs, nodeIdPair.id); if (!nodeFrom) nodeFrom = _findNodeById(doc, nodeIdPair.id); - _cloneNode(nodeFrom, nodeIdPair.node, 0); - if (nodeFrom && nodeFrom->type == SvgNodeType::Symbol && nodeIdPair.node->type == SvgNodeType::Use) { - nodeIdPair.node->node.use.symbol = nodeFrom; + if (!_findParentById(nodeIdPair.node, nodeIdPair.id, doc)) { + _cloneNode(nodeFrom, nodeIdPair.node, 0); + if (nodeFrom && nodeFrom->type == SvgNodeType::Symbol && nodeIdPair.node->type == SvgNodeType::Use) { + nodeIdPair.node->node.use.symbol = nodeFrom; + } + } else { + TVGLOG("SVG", "%s is ancestor element. This reference is invalid.", nodeIdPair.id); } free(nodeIdPair.id); } @@ -3056,6 +3203,14 @@ static void _svgLoaderParserXmlClose(SvgLoaderData* loader, const char* content) } } + for (unsigned int i = 0; i < sizeof(graphicsTags) / sizeof(graphicsTags[0]); i++) { + if (!strncmp(content, graphicsTags[i].tag, graphicsTags[i].sz - 1)) { + loader->currentGraphicsNode = nullptr; + loader->stack.pop(); + break; + } + } + loader->level--; } @@ -3122,6 +3277,11 @@ static void _svgLoaderParserXmlOpen(SvgLoaderData* loader, const char* content, if (loader->stack.count > 0) parent = loader->stack.last(); else parent = loader->doc; node = method(loader, parent, attrs, attrsLength, simpleXmlParseAttributes); + if (node && !empty) { + auto defs = _createDefsNode(loader, nullptr, nullptr, 0, nullptr); + loader->stack.push(defs); + loader->currentGraphicsNode = node; + } } else if ((gradientMethod = _findGradientFactory(tagName))) { SvgStyleGradient* gradient; gradient = gradientMethod(loader, attrs, attrsLength); @@ -3233,7 +3393,7 @@ static void _inefficientNodeCheck(TVG_UNUSED SvgNode* node) #ifdef THORVG_LOG_ENABLED auto type = simpleXmlNodeTypeToString(node->type); - if (!node->display && node->type != SvgNodeType::ClipPath && node->type != SvgNodeType::Symbol) TVGLOG("SVG", "Inefficient elements used [Display is none][Node Type : %s]", type); + if (!node->style->display && node->type != SvgNodeType::ClipPath) TVGLOG("SVG", "Inefficient elements used [Display is none][Node Type : %s]", type); if (node->style->opacity == 0) TVGLOG("SVG", "Inefficient elements used [Opacity is zero][Node Type : %s]", type); if (node->style->fill.opacity == 0 && node->style->stroke.opacity == 0) TVGLOG("SVG", "Inefficient elements used [Fill opacity and stroke opacity are zero][Node Type : %s]", type); @@ -3551,7 +3711,7 @@ SvgLoader::~SvgLoader() void SvgLoader::run(unsigned tid) { //According to the SVG standard the value of the width/height of the viewbox set to 0 disables rendering - if ((viewFlag & SvgViewFlag::Viewbox) && (fabsf(vw) <= FLT_EPSILON || fabsf(vh) <= FLT_EPSILON)) { + if ((viewFlag & SvgViewFlag::Viewbox) && (fabsf(vw) <= FLOAT_EPSILON || fabsf(vh) <= FLOAT_EPSILON)) { TVGLOG("SVG", "The width and/or height set to 0 - rendering disabled."); root = Scene::gen().release(); return; @@ -3675,10 +3835,11 @@ bool SvgLoader::open(const char* data, uint32_t size, bool copy) clear(); if (copy) { - content = (char*)malloc(size); + content = (char*)malloc(size + 1); if (!content) return false; memcpy((char*)content, data, size); - } else content = data; + content[size] = '\0'; + } else content = (char*)data; this->size = size; this->copy = copy; @@ -3702,7 +3863,7 @@ bool SvgLoader::open(const string& path) if (filePath.empty()) return false; - content = filePath.c_str(); + content = (char*)filePath.c_str(); size = filePath.size(); return header(); diff --git a/src/libs/thorvg/tvgSvgLoader.h b/src/libs/thorvg/tvgSvgLoader.h index a608260ca..d5cd7bb5e 100644 --- a/src/libs/thorvg/tvgSvgLoader.h +++ b/src/libs/thorvg/tvgSvgLoader.h @@ -34,7 +34,7 @@ class SvgLoader : public ImageLoader, public Task public: string filePath; string svgPath = ""; - const char* content = nullptr; + char* content = nullptr; uint32_t size = 0; SvgLoaderData loaderData; diff --git a/src/libs/thorvg/tvgSvgLoaderCommon.h b/src/libs/thorvg/tvgSvgLoaderCommon.h index 68f437386..b0373908b 100644 --- a/src/libs/thorvg/tvgSvgLoaderCommon.h +++ b/src/libs/thorvg/tvgSvgLoaderCommon.h @@ -488,11 +488,12 @@ struct SvgStyleProperty SvgComposite mask; int opacity; SvgColor color; - bool curColorSet; char* cssClass; - bool paintOrder; //true if default (fill, stroke), false otherwise SvgStyleFlags flags; SvgStyleFlags flagsImportance; //indicates the importance of the flag - if set, higher priority is applied (https://drafts.csswg.org/css-cascade-4/#importance) + bool curColorSet; + bool paintOrder; //true if default (fill, stroke), false otherwise + bool display; }; struct SvgNode @@ -521,7 +522,6 @@ struct SvgNode SvgCssStyleNode cssStyle; SvgSymbolNode symbol; } node; - bool display; ~SvgNode(); }; @@ -563,6 +563,7 @@ struct SvgLoaderData int level = 0; bool result = false; bool style = false; + SvgNode* currentGraphicsNode = nullptr; }; struct Box diff --git a/src/libs/thorvg/tvgSvgPath.cpp b/src/libs/thorvg/tvgSvgPath.cpp index 5c09f9337..ea1952b33 100644 --- a/src/libs/thorvg/tvgSvgPath.cpp +++ b/src/libs/thorvg/tvgSvgPath.cpp @@ -125,14 +125,11 @@ void _pathAppendArcTo(Array* cmds, Array* pts, Point* cur, P sx = cur->x; sy = cur->y; - //If start and end points are identical, then no arc is drawn - if ((fabsf(x - sx) < (1.0f / 256.0f)) && (fabsf(y - sy) < (1.0f / 256.0f))) return; - //Correction of out-of-range radii, see F6.6.1 (step 2) rx = fabsf(rx); ry = fabsf(ry); - angle = angle * M_PI / 180.0f; + angle = mathDeg2Rad(angle); cosPhi = cosf(angle); sinPhi = sinf(angle); dx2 = (sx - x) / 2.0f; @@ -201,24 +198,24 @@ void _pathAppendArcTo(Array* cmds, Array* pts, Point* cur, P //http://www.euclideanspace.com/maths/algebra/vectors/angleBetween/index.htm //Note: atan2 (0.0, 1.0) == 0.0 at = atan2(((y1p - cyp) / ry), ((x1p - cxp) / rx)); - theta1 = (at < 0.0f) ? 2.0f * M_PI + at : at; + theta1 = (at < 0.0f) ? 2.0f * MATH_PI + at : at; nat = atan2(((-y1p - cyp) / ry), ((-x1p - cxp) / rx)); - deltaTheta = (nat < at) ? 2.0f * M_PI - at + nat : nat - at; + deltaTheta = (nat < at) ? 2.0f * MATH_PI - at + nat : nat - at; if (sweep) { //Ensure delta theta < 0 or else add 360 degrees - if (deltaTheta < 0.0f) deltaTheta += (float)(2.0f * M_PI); + if (deltaTheta < 0.0f) deltaTheta += 2.0f * MATH_PI; } else { //Ensure delta theta > 0 or else substract 360 degrees - if (deltaTheta > 0.0f) deltaTheta -= (float)(2.0f * M_PI); + if (deltaTheta > 0.0f) deltaTheta -= 2.0f * MATH_PI; } //Add several cubic bezier to approximate the arc //(smaller than 90 degrees) //We add one extra segment because we want something //Smaller than 90deg (i.e. not 90 itself) - segments = static_cast(fabsf(deltaTheta / float(M_PI_2)) + 1.0f); + segments = static_cast(fabsf(deltaTheta / MATH_PI2) + 1.0f); delta = deltaTheta / segments; //http://www.stillhq.com/ctpfaq/2001/comp.text.pdf-faq-2001-04.txt (section 2.13) @@ -317,7 +314,7 @@ static int _numberCount(char cmd) } -static bool _processCommand(Array* cmds, Array* pts, char cmd, float* arr, int count, Point* cur, Point* curCtl, Point* startPoint, bool *isQuadratic) +static bool _processCommand(Array* cmds, Array* pts, char cmd, float* arr, int count, Point* cur, Point* curCtl, Point* startPoint, bool *isQuadratic, bool* closed) { switch (cmd) { case 'm': @@ -470,6 +467,7 @@ static bool _processCommand(Array* cmds, Array* pts, char cm case 'Z': { cmds->push(PathCommand::Close); *cur = *startPoint; + *closed = true; break; } case 'a': @@ -494,7 +492,7 @@ static bool _processCommand(Array* cmds, Array* pts, char cm } -static char* _nextCommand(char* path, char* cmd, float* arr, int* count) +static char* _nextCommand(char* path, char* cmd, float* arr, int* count, bool* closed) { int large, sweep; @@ -506,6 +504,9 @@ static char* _nextCommand(char* path, char* cmd, float* arr, int* count) } else { if (*cmd == 'm') *cmd = 'l'; else if (*cmd == 'M') *cmd = 'L'; + else { + if (*closed) return nullptr; + } } if (*count == 7) { //Special case for arc command @@ -554,6 +555,7 @@ bool svgPathToShape(const char* svgPath, Shape* shape) Point startPoint = { 0, 0 }; char cmd = 0; bool isQuadratic = false; + bool closed = false; char* path = (char*)svgPath; auto& pts = P(shape)->rs.path.pts; @@ -561,9 +563,10 @@ bool svgPathToShape(const char* svgPath, Shape* shape) auto lastCmds = cmds.count; while ((path[0] != '\0')) { - path = _nextCommand(path, &cmd, numberArray, &numberCount); + path = _nextCommand(path, &cmd, numberArray, &numberCount, &closed); if (!path) break; - if (!_processCommand(&cmds, &pts, cmd, numberArray, numberCount, &cur, &curCtl, &startPoint, &isQuadratic)) break; + closed = false; + if (!_processCommand(&cmds, &pts, cmd, numberArray, numberCount, &cur, &curCtl, &startPoint, &isQuadratic, &closed)) break; } if (cmds.count > lastCmds && cmds[lastCmds] != PathCommand::MoveTo) return false; diff --git a/src/libs/thorvg/tvgSvgSceneBuilder.cpp b/src/libs/thorvg/tvgSvgSceneBuilder.cpp index 64f19a866..7d13bd4d1 100644 --- a/src/libs/thorvg/tvgSvgSceneBuilder.cpp +++ b/src/libs/thorvg/tvgSvgSceneBuilder.cpp @@ -313,7 +313,7 @@ static void _applyProperty(SvgLoaderData& loaderData, SvgNode* node, Shape* vg, //Clip transformation is applied directly to the path in the _appendClipShape function if (node->transform && !clip) vg->transform(*node->transform); - if (node->type == SvgNodeType::Doc || !node->display) return; + if (node->type == SvgNodeType::Doc || !node->style->display) return; //If fill property is nullptr then do nothing if (style->fill.paint.none) { @@ -560,7 +560,7 @@ static bool _isValidImageMimeTypeAndEncoding(const char** href, const char** mim static unique_ptr _imageBuildHelper(SvgLoaderData& loaderData, SvgNode* node, const Box& vBox, const string& svgPath) { - if (!node->node.image.href) return nullptr; + if (!node->node.image.href || !strlen(node->node.image.href)) return nullptr; auto picture = Picture::gen(); TaskScheduler::async(false); //force to load a picture on the same thread @@ -785,13 +785,13 @@ static unique_ptr _sceneBuildHelper(SvgLoaderData& loaderData, const SvgN // For a Symbol node, the viewBox transformation has to be applied first - see _useBuildHelper() if (!mask && node->transform && node->type != SvgNodeType::Symbol) scene->transform(*node->transform); - if (node->display && node->style->opacity != 0) { + if (node->style->display && node->style->opacity != 0) { auto child = node->child.data; for (uint32_t i = 0; i < node->child.count; ++i, ++child) { if (_isGroupType((*child)->type)) { if ((*child)->type == SvgNodeType::Use) scene->push(_useBuildHelper(loaderData, *child, vBox, svgPath, depth + 1, isMaskWhite)); - else + else if (!((*child)->type == SvgNodeType::Symbol && node->type != SvgNodeType::Use)) scene->push(_sceneBuildHelper(loaderData, *child, vBox, svgPath, false, depth + 1, isMaskWhite)); } else if ((*child)->type == SvgNodeType::Image) { auto image = _imageBuildHelper(loaderData, *child, vBox, svgPath); diff --git a/src/libs/thorvg/tvgSvgUtil.cpp b/src/libs/thorvg/tvgSvgUtil.cpp index 2e89f8ed5..e77e2a0f7 100644 --- a/src/libs/thorvg/tvgSvgUtil.cpp +++ b/src/libs/thorvg/tvgSvgUtil.cpp @@ -50,7 +50,6 @@ size_t svgUtilURLDecode(const char *src, char** dst) if (length == 0) return 0; char* decoded = (char*)malloc(sizeof(char) * length + 1); - decoded[length] = '\0'; char a, b; int idx =0; @@ -67,9 +66,10 @@ size_t svgUtilURLDecode(const char *src, char** dst) decoded[idx++] = *src++; } } + decoded[idx] = '\0'; *dst = decoded; - return length + 1; + return idx + 1; } #endif /* LV_USE_THORVG_INTERNAL */ diff --git a/src/libs/thorvg/tvgSwCanvas.cpp b/src/libs/thorvg/tvgSwCanvas.cpp index c76079b93..27bd7e3f6 100644 --- a/src/libs/thorvg/tvgSwCanvas.cpp +++ b/src/libs/thorvg/tvgSwCanvas.cpp @@ -90,6 +90,8 @@ Result SwCanvas::target(uint32_t* buffer, uint32_t stride, uint32_t w, uint32_t if (!renderer) return Result::MemoryCorruption; if (!renderer->target(buffer, stride, w, h, static_cast(cs))) return Result::InvalidArguments; + Canvas::pImpl->vport = {0, 0, (int32_t)w, (int32_t)h}; + renderer->viewport(Canvas::pImpl->vport); //Paints must be updated again with this new target. Canvas::pImpl->needRefresh(); diff --git a/src/libs/thorvg/tvgSwFill.cpp b/src/libs/thorvg/tvgSwFill.cpp index 487cd8e26..f939fc8b2 100644 --- a/src/libs/thorvg/tvgSwFill.cpp +++ b/src/libs/thorvg/tvgSwFill.cpp @@ -143,7 +143,7 @@ bool _prepareLinear(SwFill* fill, const LinearGradient* linear, const Matrix* tr fill->linear.dy = y2 - y1; fill->linear.len = fill->linear.dx * fill->linear.dx + fill->linear.dy * fill->linear.dy; - if (fill->linear.len < FLT_EPSILON) return true; + if (fill->linear.len < FLOAT_EPSILON) return true; fill->linear.dx /= fill->linear.len; fill->linear.dy /= fill->linear.len; @@ -185,7 +185,7 @@ bool _prepareRadial(SwFill* fill, const RadialGradient* radial, const Matrix* tr auto fy = P(radial)->fy; auto fr = P(radial)->fr; - if (r < FLT_EPSILON) return true; + if (r < FLOAT_EPSILON) return true; fill->radial.dr = r - fr; fill->radial.dx = cx - fx; diff --git a/src/libs/thorvg/tvgSwRaster.cpp b/src/libs/thorvg/tvgSwRaster.cpp index 7be04e6a4..686712fc8 100644 --- a/src/libs/thorvg/tvgSwRaster.cpp +++ b/src/libs/thorvg/tvgSwRaster.cpp @@ -377,7 +377,7 @@ static bool _rasterMattedRect(SwSurface* surface, const SwBBox& region, uint8_t auto alpha = surface->alpha(surface->compositor->method); TVGLOG("SW_ENGINE", "Matted(%d) Rect [Region: %lu %lu %u %u]", (int)surface->compositor->method, region.min.x, region.min.y, w, h); - + //32bits channels if (surface->channelSize == sizeof(uint32_t)) { auto color = surface->join(r, g, b, a); @@ -719,7 +719,7 @@ static bool _rasterDirectScaledMaskedRleImage(SwSurface* surface, const SwImage* auto span = image->rle->spans; int32_t miny = 0, maxy = 0; - for (uint32_t i = 0; i < image->rle->size; ++i, ++span) { + for (uint32_t i = 0; i < image->rle->size; ++i, ++span) { SCALED_IMAGE_RANGE_Y(span->y) auto cmp = &surface->compositor->image.buf8[span->y * surface->compositor->image.stride + span->x]; auto dst = &surface->buf8[span->y * surface->stride + span->x]; @@ -1556,7 +1556,7 @@ static bool _rasterSolidGradientRect(SwSurface* surface, const SwBBox& region, c static bool _rasterLinearGradientRect(SwSurface* surface, const SwBBox& region, const SwFill* fill) { - if (fill->linear.len < FLT_EPSILON) return false; + if (fill->linear.len < FLOAT_EPSILON) return false; if (_compositing(surface)) { if (_matting(surface)) return _rasterGradientMattedRect(surface, region, fill); @@ -1758,8 +1758,13 @@ static bool _rasterRadialGradientRle(SwSurface* surface, const SwRleData* rle, c void rasterGrayscale8(uint8_t *dst, uint8_t val, uint32_t offset, int32_t len) { - //OPTIMIZE_ME: Support SIMD +#if defined(THORVG_AVX_VECTOR_SUPPORT) + avxRasterGrayscale8(dst, val, offset, len); +#elif defined(THORVG_NEON_VECTOR_SUPPORT) + neonRasterGrayscale8(dst, val, offset, len); +#else cRasterPixels(dst, val, offset, len); +#endif } diff --git a/src/libs/thorvg/tvgSwRasterAvx.h b/src/libs/thorvg/tvgSwRasterAvx.h index 260eecc44..742d7ec42 100644 --- a/src/libs/thorvg/tvgSwRasterAvx.h +++ b/src/libs/thorvg/tvgSwRasterAvx.h @@ -1,4 +1,4 @@ -/* +/* * Copyright (c) 2021 - 2024 the ThorVG project. All rights reserved. * Permission is hereby granted, free of charge, to any person obtaining a copy @@ -65,6 +65,23 @@ static inline __m128i ALPHA_BLEND(__m128i c, __m128i a) } +static void avxRasterGrayscale8(uint8_t* dst, uint8_t val, uint32_t offset, int32_t len) +{ + dst += offset; + + __m256i vecVal = _mm256_set1_epi8(val); + + int32_t i = 0; + for (; i <= len - 32; i += 32) { + _mm256_storeu_si256((__m256i*)(dst + i), vecVal); + } + + for (; i < len; ++i) { + dst[i] = val; + } +} + + static void avxRasterPixel32(uint32_t *dst, uint32_t val, uint32_t offset, int32_t len) { //1. calculate how many iterations we need to cover the length diff --git a/src/libs/thorvg/tvgSwRasterNeon.h b/src/libs/thorvg/tvgSwRasterNeon.h index fd21b4c9a..a84bfe87d 100644 --- a/src/libs/thorvg/tvgSwRasterNeon.h +++ b/src/libs/thorvg/tvgSwRasterNeon.h @@ -27,6 +27,15 @@ #include +//TODO : need to support windows ARM + +#if defined(__ARM_64BIT_STATE) || defined(_M_ARM64) +#define TVG_AARCH64 1 +#else +#define TVG_AARCH64 0 +#endif + + static inline uint8x8_t ALPHA_BLEND(uint8x8_t c, uint8x8_t a) { uint16x8_t t = vmull_u8(c, a); @@ -34,19 +43,50 @@ static inline uint8x8_t ALPHA_BLEND(uint8x8_t c, uint8x8_t a) } +static void neonRasterGrayscale8(uint8_t* dst, uint8_t val, uint32_t offset, int32_t len) +{ + dst += offset; + + int32_t i = 0; + const uint8x16_t valVec = vdupq_n_u8(val); +#if TVG_AARCH64 + uint8x16x4_t valQuad = {valVec, valVec, valVec, valVec}; + for (; i <= len - 16 * 4; i += 16 * 4) { + vst1q_u8_x4(dst + i, valQuad); + } +#else + for (; i <= len - 16; i += 16) { + vst1q_u8(dst + i, valVec); + } +#endif + for (; i < len; i++) { + dst[i] = val; + } +} + + static void neonRasterPixel32(uint32_t *dst, uint32_t val, uint32_t offset, int32_t len) { + dst += offset; + + uint32x4_t vectorVal = vdupq_n_u32(val); + +#if TVG_AARCH64 + uint32_t iterations = len / 16; + uint32_t neonFilled = iterations * 16; + uint32x4x4_t valQuad = {vectorVal, vectorVal, vectorVal, vectorVal}; + for (uint32_t i = 0; i < iterations; ++i) { + vst4q_u32(dst, valQuad); + dst += 16; + } +#else uint32_t iterations = len / 4; uint32_t neonFilled = iterations * 4; - - dst += offset; - uint32x4_t vectorVal = {val, val, val, val}; - for (uint32_t i = 0; i < iterations; ++i) { vst1q_u32(dst, vectorVal); dst += 4; } - +#endif int32_t leftovers = len - neonFilled; while (leftovers--) *dst++ = val; } @@ -59,7 +99,7 @@ static bool neonRasterTranslucentRle(SwSurface* surface, const SwRleData* rle, u return false; } - auto color = surface->blender.join(r, g, b, a); + auto color = surface->join(r, g, b, a); auto span = rle->spans; uint32_t src; uint8x8_t *vDst = nullptr; @@ -70,9 +110,9 @@ static bool neonRasterTranslucentRle(SwSurface* surface, const SwRleData* rle, u else src = color; auto dst = &surface->buf32[span->y * surface->stride + span->x]; - auto ialpha = IALPHA(src); + auto ialpha = IA(src); - if ((((uint32_t) dst) & 0x7) != 0) { + if ((((uintptr_t) dst) & 0x7) != 0) { //fill not aligned byte *dst = src + ALPHA_BLEND(*dst, ialpha); vDst = (uint8x8_t*)(dst + 1); @@ -104,7 +144,7 @@ static bool neonRasterTranslucentRect(SwSurface* surface, const SwBBox& region, return false; } - auto color = surface->blender.join(r, g, b, a); + auto color = surface->join(r, g, b, a); auto buffer = surface->buf32 + (region.min.y * surface->stride) + region.min.x; auto h = static_cast(region.max.y - region.min.y); auto w = static_cast(region.max.x - region.min.x); @@ -119,7 +159,7 @@ static bool neonRasterTranslucentRect(SwSurface* surface, const SwBBox& region, for (uint32_t y = 0; y < h; ++y) { auto dst = &buffer[y * surface->stride]; - if ((((uint32_t) dst) & 0x7) != 0) { + if ((((uintptr_t) dst) & 0x7) != 0) { //fill not aligned byte *dst = color + ALPHA_BLEND(*dst, ialpha); vDst = (uint8x8_t*) (dst + 1); diff --git a/src/libs/thorvg/tvgSwRenderer.cpp b/src/libs/thorvg/tvgSwRenderer.cpp index 56214d759..f793bf64a 100644 --- a/src/libs/thorvg/tvgSwRenderer.cpp +++ b/src/libs/thorvg/tvgSwRenderer.cpp @@ -437,10 +437,6 @@ bool SwRenderer::target(pixel_t* data, uint32_t stride, uint32_t w, uint32_t h, surface->channelSize = CHANNEL_SIZE(cs); surface->premultiplied = true; - vport.x = vport.y = 0; - vport.w = surface->w; - vport.h = surface->h; - return rasterCompositor(surface); } @@ -614,6 +610,12 @@ bool SwRenderer::mempool(bool shared) } +const Surface* SwRenderer::mainSurface() +{ + return surface; +} + + Compositor* SwRenderer::target(const RenderRegion& region, ColorSpace cs) { auto x = region.x; diff --git a/src/libs/thorvg/tvgSwRenderer.h b/src/libs/thorvg/tvgSwRenderer.h index 482404aa3..593ba52e4 100644 --- a/src/libs/thorvg/tvgSwRenderer.h +++ b/src/libs/thorvg/tvgSwRenderer.h @@ -52,6 +52,7 @@ public: bool viewport(const RenderRegion& vp) override; bool blend(BlendMethod method) override; ColorSpace colorSpace() override; + const Surface* mainSurface() override; bool clear() override; bool sync() override; diff --git a/src/libs/thorvg/tvgSwShape.cpp b/src/libs/thorvg/tvgSwShape.cpp index ef9c16227..bdfabb61c 100644 --- a/src/libs/thorvg/tvgSwShape.cpp +++ b/src/libs/thorvg/tvgSwShape.cpp @@ -25,56 +25,41 @@ #include "tvgSwCommon.h" #include "tvgMath.h" -#include "tvgBezier.h" +#include "tvgLines.h" /************************************************************************/ /* Internal Class Implementation */ /************************************************************************/ -struct Line +static bool _outlineBegin(SwOutline& outline) { - Point pt1; - Point pt2; -}; - - -static float _lineLength(const Point& pt1, const Point& pt2) -{ - Point diff = {pt2.x - pt1.x, pt2.y - pt1.y}; - return sqrtf(diff.x * diff.x + diff.y * diff.y); -} - - -static void _lineSplitAt(const Line& cur, float at, Line& left, Line& right) -{ - auto len = _lineLength(cur.pt1, cur.pt2); - auto dx = ((cur.pt2.x - cur.pt1.x) / len) * at; - auto dy = ((cur.pt2.y - cur.pt1.y) / len) * at; - left.pt1 = cur.pt1; - left.pt2.x = left.pt1.x + dx; - left.pt2.y = left.pt1.y + dy; - right.pt1 = left.pt2; - right.pt2 = cur.pt2; -} - - -static void _outlineEnd(SwOutline& outline) -{ - if (outline.pts.empty()) return; + //Make a contour if lineTo/curveTo without calling close or moveTo beforehand. + if (outline.pts.empty()) return false; outline.cntrs.push(outline.pts.count - 1); outline.closed.push(false); + outline.pts.push(outline.pts[outline.cntrs.last()]); + outline.types.push(SW_CURVE_TYPE_POINT); + return false; } -static void _outlineMoveTo(SwOutline& outline, const Point* to, const Matrix* transform) +static bool _outlineEnd(SwOutline& outline) { - if (outline.pts.count > 0) { - outline.cntrs.push(outline.pts.count - 1); - outline.closed.push(false); - } + if (outline.pts.empty()) return false; + outline.cntrs.push(outline.pts.count - 1); + outline.closed.push(false); + return false; +} + + +static bool _outlineMoveTo(SwOutline& outline, const Point* to, const Matrix* transform, bool closed = false) +{ + //make it a contour, if the last contour is not closed yet. + if (!closed) _outlineEnd(outline); outline.pts.push(mathTransform(to, transform)); outline.types.push(SW_CURVE_TYPE_POINT); + return false; } @@ -91,37 +76,40 @@ static void _outlineCubicTo(SwOutline& outline, const Point* ctrl1, const Point* outline.types.push(SW_CURVE_TYPE_CUBIC); outline.pts.push(mathTransform(ctrl2, transform)); - outline.types.push(SW_CURVE_TYPE_CUBIC); + outline.types.push(SW_CURVE_TYPE_CUBIC); outline.pts.push(mathTransform(to, transform)); outline.types.push(SW_CURVE_TYPE_POINT); } -static void _outlineClose(SwOutline& outline) +static bool _outlineClose(SwOutline& outline) { - uint32_t i = 0; - + uint32_t i; if (outline.cntrs.count > 0) i = outline.cntrs.last() + 1; - else i = 0; //First Path + else i = 0; //Make sure there is at least one point in the current path - if (outline.pts.count == i) return; + if (outline.pts.count == i) return false; //Close the path outline.pts.push(outline.pts[i]); + outline.cntrs.push(outline.pts.count - 1); outline.types.push(SW_CURVE_TYPE_POINT); outline.closed.push(true); + + return true; } static void _dashLineTo(SwDashStroke& dash, const Point* to, const Matrix* transform) { Line cur = {dash.ptCur, *to}; - auto len = _lineLength(cur.pt1, cur.pt2); + auto len = lineLength(cur.pt1, cur.pt2); if (mathZero(len)) { _outlineMoveTo(*dash.outline, &dash.ptCur, transform); + //draw the current line fully } else if (len < dash.curLen) { dash.curLen -= len; if (!dash.curOpGap) { @@ -131,14 +119,15 @@ static void _dashLineTo(SwDashStroke& dash, const Point* to, const Matrix* trans } _outlineLineTo(*dash.outline, to, transform); } + //draw the current line partially } else { while (len - dash.curLen > 0.0001f) { Line left, right; if (dash.curLen > 0) { len -= dash.curLen; - _lineSplitAt(cur, dash.curLen, left, right); + lineSplitAt(cur, dash.curLen, left, right); if (!dash.curOpGap) { - if (dash.move || dash.pattern[dash.curIdx] - dash.curLen < FLT_EPSILON) { + if (dash.move || dash.pattern[dash.curIdx] - dash.curLen < FLOAT_EPSILON) { _outlineMoveTo(*dash.outline, &left.pt1, transform); dash.move = false; } @@ -179,6 +168,7 @@ static void _dashCubicTo(SwDashStroke& dash, const Point* ctrl1, const Point* ct Bezier cur = {dash.ptCur, *ctrl1, *ctrl2, *to}; auto len = bezLength(cur); + //draw the current line fully if (mathZero(len)) { _outlineMoveTo(*dash.outline, &dash.ptCur, transform); } else if (len < dash.curLen) { @@ -190,6 +180,7 @@ static void _dashCubicTo(SwDashStroke& dash, const Point* ctrl1, const Point* ct } _outlineCubicTo(*dash.outline, ctrl1, ctrl2, to, transform); } + //draw the current line partially } else { while ((len - dash.curLen) > 0.0001f) { Bezier left, right; @@ -197,7 +188,7 @@ static void _dashCubicTo(SwDashStroke& dash, const Point* ctrl1, const Point* ct len -= dash.curLen; bezSplitAt(cur, dash.curLen, left, right); if (!dash.curOpGap) { - if (dash.move || dash.pattern[dash.curIdx] - dash.curLen < FLT_EPSILON) { + if (dash.move || dash.pattern[dash.curIdx] - dash.curLen < FLOAT_EPSILON) { _outlineMoveTo(*dash.outline, &left.start, transform); dash.move = false; } @@ -239,7 +230,15 @@ static void _dashClose(SwDashStroke& dash, const Matrix* transform) } -static void _dashMoveTo(SwDashStroke& dash, uint32_t offIdx, float offset, const Point* pts, const Matrix* transform) +static void _dashMoveTo(SwDashStroke& dash, const Point* pts) +{ + dash.ptCur = *pts; + dash.ptStart = *pts; + dash.move = true; +} + + +static void _dashMoveTo(SwDashStroke& dash, uint32_t offIdx, float offset, const Point* pts) { dash.curIdx = offIdx % dash.cnt; dash.curLen = dash.pattern[dash.curIdx] - offset; @@ -274,7 +273,7 @@ static SwOutline* _genDashOutline(const RenderShape* rshape, const Matrix* trans //default if (end > begin) { - if (begin > 0) dash.cnt += 4; + if (begin > 0.0f) dash.cnt += 4; else dash.cnt += 2; //looping } else dash.cnt += 3; @@ -292,7 +291,7 @@ static SwOutline* _genDashOutline(const RenderShape* rshape, const Matrix* trans dash.pattern[0] = 0; //zero dash to start with a space. dash.pattern[1] = begin; dash.pattern[2] = end - begin; - dash.pattern[3] = length - (end - begin); + dash.pattern[3] = length - end; } trimmed = true; @@ -309,7 +308,7 @@ static SwOutline* _genDashOutline(const RenderShape* rshape, const Matrix* trans bool isOdd = dash.cnt % 2; if (isOdd) patternLength *= 2; - offset = fmod(offset, patternLength); + offset = fmodf(offset, patternLength); if (offset < 0) offset += patternLength; for (size_t i = 0; i < dash.cnt * (1 + (size_t)isOdd); ++i, ++offIdx) { @@ -321,14 +320,22 @@ static SwOutline* _genDashOutline(const RenderShape* rshape, const Matrix* trans dash.outline = mpoolReqDashOutline(mpool, tid); - while (cmdCnt-- > 0) { + //must begin with moveTo + if (cmds[0] == PathCommand::MoveTo) { + _dashMoveTo(dash, offIdx, offset, pts); + cmds++; + pts++; + } + + while (--cmdCnt > 0) { switch (*cmds) { case PathCommand::Close: { _dashClose(dash, transform); break; } case PathCommand::MoveTo: { - _dashMoveTo(dash, offIdx, offset, pts, transform); + if (rshape->stroke->trim.individual) _dashMoveTo(dash, pts); + else _dashMoveTo(dash, offIdx, offset, pts); ++pts; break; } @@ -366,13 +373,19 @@ static float _outlineLength(const RenderShape* rshape) const Point* close = nullptr; auto length = 0.0f; + auto slength = -1.0f; + auto simultaneous = !rshape->stroke->trim.individual; //Compute the whole length while (cmdCnt-- > 0) { switch (*cmds) { case PathCommand::Close: { length += mathLength(pts - 1, close); - ++pts; + //retrieve the max length of the shape if the simultaneous mode. + if (simultaneous) { + if (slength < length) slength = length; + length = 0.0f; + } break; } case PathCommand::MoveTo: { @@ -393,7 +406,8 @@ static float _outlineLength(const RenderShape* rshape) } ++cmds; } - return length; + if (simultaneous && slength > length) return slength; + else return length; } @@ -428,25 +442,28 @@ static bool _genOutline(SwShape* shape, const RenderShape* rshape, const Matrix* shape->outline = mpoolReqOutline(mpool, tid); auto outline = shape->outline; + auto closed = false; //Generate Outlines while (cmdCnt-- > 0) { switch (*cmds) { case PathCommand::Close: { - _outlineClose(*outline); + if (!closed) closed = _outlineClose(*outline); break; } case PathCommand::MoveTo: { - _outlineMoveTo(*outline, pts, transform); + closed = _outlineMoveTo(*outline, pts, transform, closed); ++pts; break; } case PathCommand::LineTo: { + if (closed) closed = _outlineBegin(*outline); _outlineLineTo(*outline, pts, transform); ++pts; break; } case PathCommand::CubicTo: { + if (closed) closed = _outlineBegin(*outline); _outlineCubicTo(*outline, pts, pts + 1, pts + 2, transform); pts += 3; break; @@ -455,7 +472,7 @@ static bool _genOutline(SwShape* shape, const RenderShape* rshape, const Matrix* ++cmds; } - _outlineEnd(*outline); + if (!closed) _outlineEnd(*outline); outline->fillRule = rshape->rule; shape->outline = outline; diff --git a/src/libs/thorvg/tvgWgCanvas.cpp b/src/libs/thorvg/tvgWgCanvas.cpp index 6387317fb..3bdaec9e4 100644 --- a/src/libs/thorvg/tvgWgCanvas.cpp +++ b/src/libs/thorvg/tvgWgCanvas.cpp @@ -66,6 +66,8 @@ Result WgCanvas::target(void* window, uint32_t w, uint32_t h) noexcept if (!renderer) return Result::MemoryCorruption; if (!renderer->target(window, w, h)) return Result::Unknown; + Canvas::pImpl->vport = {0, 0, (int32_t)w, (int32_t)h}; + renderer->viewport(Canvas::pImpl->vport); //Paints must be updated again with this new target. Canvas::pImpl->needRefresh(); diff --git a/src/libs/thorvg/tvgXmlParser.cpp b/src/libs/thorvg/tvgXmlParser.cpp index 194aebc2e..ffb9b5df4 100644 --- a/src/libs/thorvg/tvgXmlParser.cpp +++ b/src/libs/thorvg/tvgXmlParser.cpp @@ -317,7 +317,10 @@ bool simpleXmlParseAttributes(const char* buf, unsigned bufLength, simpleXMLAttr if ((*keyEnd == '=') || (isspace((unsigned char)*keyEnd))) break; } if (keyEnd == itrEnd) goto error; - if (keyEnd == key) continue; + if (keyEnd == key) { // There is no key. This case is invalid, but explores the following syntax. + itr = keyEnd + 1; + continue; + } if (*keyEnd == '=') value = keyEnd + 1; else { diff --git a/src/libs/thorvg/tvgXmlParser.h b/src/libs/thorvg/tvgXmlParser.h index b24fac017..be0806860 100644 --- a/src/libs/thorvg/tvgXmlParser.h +++ b/src/libs/thorvg/tvgXmlParser.h @@ -28,9 +28,9 @@ #include "tvgSvgLoaderCommon.h" -#define NUMBER_OF_XML_ENTITIES 8 -const char* const xmlEntity[] = {""", " ", "'", "&", "<", ">", "#", "'"}; -const int xmlEntityLength[] = {6, 6, 6, 5, 4, 4, 6, 6}; +#define NUMBER_OF_XML_ENTITIES 9 +const char* const xmlEntity[] = {" ", """, " ", "'", "&", "<", ">", "#", "'"}; +const int xmlEntityLength[] = {5, 6, 6, 6, 5, 4, 4, 6, 6}; enum class SimpleXMLType { diff --git a/tests/ref_imgs/widgets/lottie_2.png b/tests/ref_imgs/widgets/lottie_2.png index 619e06bc33e991ca90061eb7a040cff07cd04c10..2d0aa41b1b5c963625d49bacacf56c9c7a2e695e 100644 GIT binary patch delta 2276 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 literal 3230 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 literal 6779 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 diff --git a/tests/ref_imgs/widgets/lottie_3_small.png b/tests/ref_imgs/widgets/lottie_3_small.png index b2c5abc53b376d590d529b92b152ab304a091810..832e531cc3c2e5de7a570600d8ddc83215b9e8e9 100644 GIT binary patch delta 2406 zcmV-s37PiCBD*4xB!2=)L_t(|obBCdj9ukn$KiADc)X82xF*E71lqJhX-LzAiU6Xl z2^6!Wq)@QIVB@faLO_tTRfT9;0WoW6l&CCb3)t8gOC$m+HAoa_qF59Yxg^jaHY*N} z@ji^lv)p_7!NZJg?D5$C`;L`9&!=+I}&;iVT}SoY$|%KJ_AYID)NU#0cLKRWT)BmQEjmj&*wAd+3x;pM30*cE5l| z5Lm#_d+gumFRDG+G`O_dY;$03K0b5&X;+{3^&O4;@e4nm{ma{nN>Qm)Zu!DZhky3) zJzXxq&iD=%FqFExyDxt7(ysAcO-)Bf9xr{+t>8}a`+!>pS`-+eqi1IEh;KSQ50oaF8l4W!wZ20>~#VQ7&2`i zw9R_<^2+$FUM$;-&v$?3y0flpZf+jn+IdeswqV)951QJGqL@^Ce7j@5Irs0EP5kxv z(c?QeY-(Hm!2vz{pLOzCx#a`wkUtRuL*u;_fA4+$rE9Ar`oGCu@2Z_vo;u@?HyV7y`9gZR*q0DGWlhW7=X@^}Kv&PZ_O}yPmPOh7aU1Ik&UjQ^0S)6uqrecj`??oQ2o>HN#LsjCOSI7eLpL&6Rg z&`|HRzFRGNzYcZrxakAmi(&A-I{&h^2_N*GmhI)kfBAq1=>S8;4i?ZjH$HZAby#m* zl^e?`$4`k&J9GXeTh-OZ;*lv2sw-d!*}(!D=hYFf^&Owyd}?IQnG3$UaY9G`e|dX3 z_ww#|{?IIJFVA%?D*q{le3x@A-r?7S7tZUw>UyJ}4elSHM2N4i?ZLufOqn zQ*(9m4HPOJ+x~WU{k$9gxAl!mSLGWAUvtFgju^~@7d}3-v$d=5wAxmE^sgRrzbjy0 zU-+gKPl1qN_wOXwfSUte55LiHibQb+zmik!BfBxINdrJ40 z%6jkqR#7SbXTmT3cJ5W%ooCvj>D?_`)YbZ7bI<<4He=?${rKivnO2*tFE4$0PnQm` ziv$+XAf45ozT=6l6SsMH)|nSyHf~ngQ}26a)&6UyKXK8P`%kr6ZQ7#gwU+)d!`Ba+ zd(Oij{j81EjeW;sMvd9?f5ijr5`hIYNLlF*qpt3*j}AEL=#y?cY<7K9e^GUf>zVe{ z^fhbN6h%?1)z1Inj4kVG#JUlWY_G10qUfpiPjbM-1NMCJ0J}tB0Sz*-=lA-KUwG@q z?T$J469+$Z()~qyrT?W-mCJs5#ge5Uxe~vz@)>QT#Pn@{z zZW+MdCa{18IdcCm^c}xbU$&!RW5$eGaMq)v*N(1y_(16D@TOZ<+)<3_Z>G^}M?HM* z{R4bft-1G0T4k*qU>Lw&BCvo4`Qi~L^)=J_5p4qxe(>D;4q7$2*j^otT|N52bLR|j z?aKF8HVyyqCV!o~f3Z@4JteS!25D_=t@nKR)u_?+j*gB2hZna^yXBa+qx%1~uqcY6 z7`LkB{;Bs3eBr&%&M7OsS50Vd-Oc3!>;ZuVG|s5D4t=#&<+jctssi2g#{;B7lu8isZhpmFSe4P0 zn;skJcI|yy#J29!^QFNIespdBV|ET%ckpg35@62>ETEx2)AlEQ$3Jgk8pshJnQviQ{r zuk}4odh(h{&CSiJ$pLnWzyccWn#ou7-G8AzwtnH`GxvPXH{X8q_T}HHeE1`+zOnwD z^Y7gAX#sYRzyccW=s!RDfYtqXSMO}?nz>-smd_mAe^rZLS^N*LU0)sEwE48Gm9szV zkAuKa5?H_x`0liS9ldsB-|5EvHl6qM1+V?;wcVKNj)iyKvEr`ku->0$m7+MV_V)InUIXy|)cK1p>}l!WxGL-Aq}3mr{grQz8Z~NwtNJWix@7iSw^v5@ zKe(_gf6F7=j=1*hYw~La*cAc`7&5h5ZRQh~bnVwwZt+Z*F_!`{(0sn0(;m zy}L4iT_mu8eaqYLzJ1?+&3R|!yWJyeP0dZ^*11*I%Cfs0(b@dvac7)!$~i-|NPvL} zC@f&#wQk+I74NTDwQkk&)yuo?3};?00?uVQL|wV&jPXV9|0Ej Ye}xeGa)g$!>Hq)$07*qoM6N<$f}sf>!2kdN delta 2419 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 diff --git a/tests/ref_imgs/widgets/lottie_4.png b/tests/ref_imgs/widgets/lottie_4.png index d222f7672c3a46335719f0e9e979cc072d8862d6..901272543c1e07deaf4a8a11a0dfd0fd63ce603b 100644 GIT binary patch literal 7294 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�g}<^8jbYpS{^Uun`=Q)F9c#sn^G`8dwXtjgZqJ79DMHeFY<<` z{}$Ti*$lr%`82c?K;5t~4&GU=)o3`UhT&&0)_MMP7#83LpsxVhyX;ZRJKGL5lyU3v z`^wtYM611#F8iNX>?h~KH>;xx!4iWv-l*hbPh5dM*)gzlJ6^b5qlFQ@s28QI{H#Pv z<*XLl+#G{$CcVyt(Y?E5)tvx@&Hk(J+Q!1uN8-X(j3+hdX4MpeUQny$PcsMvqQ>a> zcF{qe|384;N`8tj8>G8_KD@s?>@4}z(<4M$k0UGblCj0109qbUz_ot+e7AeHN2N*h zAb()mD4Nw#hc8>n3q~V#?NF%-+Pf~Q_;y-$^wq`|h zQ+*AXp_+N0|8DqAgMc7B-U1Y{)E|3(ver>J>m7liNlXzmT}1rU2W`UCkfNvmXO+ti z+q(4e_y?$e~o()}1$rgMFpbzuZm0g9zv|GYF z7&k&ca)18gr-_ih=D4ehzz`+nqf!gl+Fi?f{9^TNPjt#vYxv}Si1`KMGSi07D&7Fr z#!IQ`&f2eCE4C}pqU z>|n;6-Q{PBMP&)=+8@$3Ch+l`&)i_2EcBHGia z8MhEC2JV_bbqxa}-sy!1W64*wI(8SX);pnes&w3<(L#6N_(AVt9AlRD0D>_U)r5O$ zb}mX`Ix&pl{amh15M%+H720=v96WkFMGmTEnZ8<`5}YHQfBX^u)^rA3X!Mq;Eu_ZltbyVGm>~oxb98+JZ~1QUq6&`nAKff!Vrp z9IT$M3*>)*k4dA|((>}?Fgj-MovGVx8)Mrtz_&Yv@qPs{v8M*$<8YLY5ZPg4*Q^}faVwnr zt=xniP+p0vmkWbRt-Q=jL=6fDm2a1>pJcfct(a^_nKhs>XA&02&Z_C@5h`;vN8v}9 z^V4l$N31&U^Ta1rQt>4hqQXkv3bOB!K1#B4@>qXThs~2Ozp$Lms07dfN1PZGfK8)` zZF)<~Rp@awy*BSs&X?-8hopNGC)I;>UgC0byv#)o`mn&lUhQG>Fx@S<^uQrtV-Nb+ z8tTCzOgLo?(oz)`lr!Hj+a`mHgpUQ(Cw z44S9ICoo8;RX^>5JY)imHPvFCjt3wxh48oZ@KC%edeBY06YQt1txfx|ZC}t$HEtiK z8$6){3e_VcBQdlcoM9emohB-DLWbgl%Dtkq}ASzHP`pG+q+7i9S z8rhdn3k!>xHB%6AY@oC(gfL!!iNA*IT^f&*4AC*#m>( zYSxDKTvMongPTGFC_^6f#n81)5UA^bD#-SG=^;SfpaVzU|2_3D692DqVqGFDI#+eR Vl~eQs@LdsPciHhr><|9G{2$K*3}FBO literal 7285 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