├── .clang-format ├── .clangd ├── .github └── workflows │ └── cmake-multi-platform.yml ├── .gitignore ├── CMakeLists.txt ├── LICENSE ├── README.md ├── cmake ├── basic.cmake └── esp_idf.cmake ├── example ├── .gitignore ├── CMakeLists.txt ├── basic │ ├── animate.cpp │ ├── animate_color.cpp │ ├── animate_value.cpp │ ├── animate_value_options.cpp │ ├── animte_sequence.cpp │ ├── basic_animations.cpp │ ├── bubbles.cpp │ ├── multi_cursor.cpp │ └── ui_hal.cpp ├── dev_zone.cpp ├── event │ ├── event_queue.cpp │ └── signal.cpp ├── smooth_lvgl │ ├── lvgl_cpp.cpp │ └── number_flow.cpp └── utils │ ├── lv_conf.h │ ├── lvgl_wrapper.h │ └── raylib_wrapper.h ├── library.json ├── library.properties ├── src ├── animation │ ├── animate │ │ ├── animate.cpp │ │ └── animate.h │ ├── animate_value │ │ ├── animate_value.cpp │ │ └── animate_value.h │ ├── generators │ │ ├── easing │ │ │ ├── easing.cpp │ │ │ └── easing.h │ │ ├── generators.h │ │ └── spring │ │ │ ├── spring.cpp │ │ │ └── spring.h │ └── sequence │ │ ├── animate_sequence.cpp │ │ └── animate_sequence.h ├── lvgl │ ├── lvgl_cpp │ │ ├── button.h │ │ ├── calendar.h │ │ ├── canvas.h │ │ ├── chart.h │ │ ├── image.h │ │ ├── label.h │ │ ├── line.h │ │ ├── lv_wrapper.h │ │ ├── obj.h │ │ ├── roller.h │ │ ├── slider.h │ │ ├── spinner.h │ │ ├── switch.h │ │ └── text_area.h │ ├── number_flow │ │ ├── digit_flow.h │ │ └── number_flow.h │ └── smooth_lvgl.h ├── smooth_ui_toolkit.cpp ├── smooth_ui_toolkit.h └── utils │ ├── color │ ├── color.cpp │ └── color.h │ ├── easing │ ├── cubic_bezier │ │ ├── cubic_bezier.cpp │ │ └── cubic_bezier.h │ ├── ease.cpp │ └── ease.h │ ├── event │ ├── event_queue.h │ └── signal.h │ ├── fpm │ ├── fixed.hpp │ ├── ios.hpp │ ├── math.hpp │ └── readme.md │ ├── hal │ ├── hal.cpp │ └── hal.h │ └── ring_buffer │ └── ring_buffer.h └── test ├── CMakeLists.txt └── ringbuffer_test.cpp /.clang-format: -------------------------------------------------------------------------------- 1 | Language: Cpp 2 | # BasedOnStyle: LLVM 3 | # 访问说明符(public、private等)的偏移 4 | AccessModifierOffset: -4 5 | # 开括号(开圆括号、开尖括号、开方括号)后的对齐 6 | AlignAfterOpenBracket: Align 7 | # 连续赋值时,等号对齐 8 | AlignConsecutiveAssignments: false 9 | # 连续赋值时,变量名对齐 10 | AlignConsecutiveDeclarations: false 11 | # 左对齐逃脱换行(使用反斜杠换行)的反斜杠 12 | AlignEscapedNewlinesLeft: true 13 | # 水平对齐二元和三元表达式的操作数 14 | AlignOperands: true 15 | # 对齐连续的尾随的注释 16 | AlignTrailingComments: true 17 | # 允许函数声明的所有参数在放在下一行 18 | AllowAllParametersOfDeclarationOnNextLine: false 19 | # 允许短的块放在同一行 20 | AllowShortBlocksOnASingleLine: false 21 | # 允许短的case标签放在同一行 22 | AllowShortCaseLabelsOnASingleLine: false 23 | # 允许短的函数放在同一行: None, InlineOnly(定义在类中), Empty(空函数), Inline(定义在类中,空函数), All 24 | AllowShortFunctionsOnASingleLine: Empty 25 | # 允许短的if语句保持在同一行 26 | AllowShortIfStatementsOnASingleLine: false 27 | # 允许短的循环保持在同一行 28 | AllowShortLoopsOnASingleLine: false 29 | # 总是在定义返回类型后换行(deprecated) 30 | AlwaysBreakAfterDefinitionReturnType: None 31 | # 总是在返回类型后换行: None, All, TopLevel(顶级函数,不包括在类中的函数), 32 | # AllDefinitions(所有的定义,不包括声明), TopLevelDefinitions(所有的顶级函数的定义) 33 | AlwaysBreakAfterReturnType: None 34 | # 总是在多行string字面量前换行 35 | AlwaysBreakBeforeMultilineStrings: true 36 | # 总是在template声明后换行 37 | AlwaysBreakTemplateDeclarations: true 38 | # false表示函数实参要么都在同一行,要么都各自一行 39 | BinPackArguments: true 40 | # false表示所有形参要么都在同一行,要么都各自一行 41 | BinPackParameters: false 42 | # 大括号换行,只有当BreakBeforeBraces设置为Custom时才有效 43 | BraceWrapping: 44 | AfterClass: false 45 | AfterControlStatement: false 46 | AfterEnum: false 47 | AfterFunction: true 48 | AfterNamespace: false 49 | AfterObjCDeclaration: false 50 | AfterStruct: false 51 | AfterUnion: false 52 | BeforeCatch: false 53 | BeforeElse: false 54 | IndentBraces: false 55 | # 在二元运算符前换行: None(在操作符后换行), NonAssignment(在非赋值的操作符前换行), All(在操作符前换行) 56 | BreakBeforeBinaryOperators: None 57 | # 在大括号前换行: Attach(始终将大括号附加到周围的上下文), Linux(除函数、命名空间和类定义,与Attach类似), 58 | # Mozilla(除枚举、函数、记录定义,与Attach类似), Stroustrup(除函数定义、catch、else,与Attach类似), 59 | # Allman(总是在大括号前换行), GNU(总是在大括号前换行,并对于控制语句的大括号增加额外的缩进), WebKit(在函数前换行), Custom 60 | # 注:这里认为语句块也属于函数 61 | BreakBeforeBraces: Custom 62 | # 在三元运算符前换行 63 | BreakBeforeTernaryOperators: true 64 | # 在构造函数的初始化列表的逗号前换行 65 | BreakConstructorInitializersBeforeComma: false 66 | # 每行字符的限制,0表示没有限制 67 | ColumnLimit: 120 68 | # 描述具有特殊意义的注释的正则表达式,它不应该被分割为多行或以其它方式改变 69 | CommentPragmas: "^ IWYU pragma:" 70 | # 构造函数的初始化列表要么都在同一行,要么都各自一行 71 | ConstructorInitializerAllOnOneLineOrOnePerLine: true 72 | # 构造函数的初始化列表的缩进宽度 73 | ConstructorInitializerIndentWidth: 4 74 | # 延续的行的缩进宽度 75 | ContinuationIndentWidth: 4 76 | # 去除C++11的列表初始化的大括号{后和}前的空格 77 | Cpp11BracedListStyle: true 78 | # 继承最常用的指针和引用的对齐方式 79 | DerivePointerAlignment: false 80 | # 关闭格式化 81 | DisableFormat: false 82 | # 自动检测函数的调用和定义是否被格式为每行一个参数(Experimental) 83 | ExperimentalAutoDetectBinPacking: false 84 | # 需要被解读为foreach循环而不是函数调用的宏 85 | ForEachMacros: [foreach, Q_FOREACH, BOOST_FOREACH] 86 | # 对#include进行排序,匹配了某正则表达式的#include拥有对应的优先级,匹配不到的则默认优先级为INT_MAX(优先级越小排序越靠前), 87 | # 可以定义负数优先级从而保证某些#include永远在最前面 88 | IncludeCategories: 89 | - Regex: '^"(llvm|llvm-c|clang|clang-c)/' 90 | Priority: 2 91 | - Regex: '^(<|"(gtest|isl|json)/)' 92 | Priority: 3 93 | - Regex: ".*" 94 | Priority: 1 95 | # 缩进case标签 96 | IndentCaseLabels: true 97 | # 缩进宽度 98 | IndentWidth: 4 99 | # 函数返回类型换行时,缩进函数声明或函数定义的函数名 100 | IndentWrappedFunctionNames: true 101 | # 保留在块开始处的空行 102 | KeepEmptyLinesAtTheStartOfBlocks: true 103 | # 开始一个块的宏的正则表达式 104 | MacroBlockBegin: "" 105 | # 结束一个块的宏的正则表达式 106 | MacroBlockEnd: "" 107 | # 连续空行的最大数量 108 | MaxEmptyLinesToKeep: 1 109 | # 命名空间的缩进: None, Inner(缩进嵌套的命名空间中的内容), All 110 | NamespaceIndentation: None 111 | # 使用ObjC块时缩进宽度 112 | ObjCBlockIndentWidth: 4 113 | # 在ObjC的@property后添加一个空格 114 | ObjCSpaceAfterProperty: false 115 | # 在ObjC的protocol列表前添加一个空格 116 | ObjCSpaceBeforeProtocolList: true 117 | # 在call(后对函数调用换行的penalty 118 | PenaltyBreakBeforeFirstCallParameter: 19 119 | # 在一个注释中引入换行的penalty 120 | PenaltyBreakComment: 300 121 | # 第一次在<<前换行的penalty 122 | PenaltyBreakFirstLessLess: 120 123 | # 在一个字符串字面量中引入换行的penalty 124 | PenaltyBreakString: 1000 125 | # 对于每个在行字符数限制之外的字符的penalty 126 | PenaltyExcessCharacter: 1000000 127 | # 将函数的返回类型放到它自己的行的penalty 128 | PenaltyReturnTypeOnItsOwnLine: 6000 129 | # 指针和引用的对齐: Left, Right, Middle 130 | PointerAlignment: Left 131 | # 允许重新排版注释 132 | ReflowComments: true 133 | # 允许排序#include 134 | SortIncludes: false 135 | # 在C风格类型转换后添加空格 136 | SpaceAfterCStyleCast: false 137 | # 在赋值运算符之前添加空格 138 | SpaceBeforeAssignmentOperators: true 139 | # 开圆括号之前添加一个空格: Never, ControlStatements, Always 140 | SpaceBeforeParens: ControlStatements 141 | # 在空的圆括号中添加空格 142 | SpaceInEmptyParentheses: false 143 | # 在尾随的评论前添加的空格数(只适用于//) 144 | SpacesBeforeTrailingComments: 1 145 | # 在尖括号的<后和>前添加空格 146 | SpacesInAngles: false 147 | # 在容器(ObjC和JavaScript的数组和字典等)字面量中添加空格 148 | SpacesInContainerLiterals: true 149 | # 在C风格类型转换的括号中添加空格 150 | SpacesInCStyleCastParentheses: false 151 | # 在圆括号的(后和)前添加空格 152 | SpacesInParentheses: false 153 | # 在方括号的[后和]前添加空格,lamda表达式和未指明大小的数组的声明不受影响 154 | SpacesInSquareBrackets: false 155 | # 标准: Cpp03, Cpp11, Auto 156 | Standard: Cpp11 157 | # tab宽度 158 | TabWidth: 4 159 | # 使用tab字符: Never, ForIndentation, ForContinuationAndIndentation, Always 160 | UseTab: Never -------------------------------------------------------------------------------- /.clangd: -------------------------------------------------------------------------------- 1 | InlayHints: 2 | Enabled: false -------------------------------------------------------------------------------- /.github/workflows/cmake-multi-platform.yml: -------------------------------------------------------------------------------- 1 | name: CMake on multiple platforms 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | jobs: 10 | build: 11 | runs-on: ${{ matrix.os }} 12 | 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | os: [ubuntu-latest, macos-latest] 17 | build_type: [Release] 18 | c_compiler: [gcc, clang] 19 | include: 20 | - os: ubuntu-latest 21 | c_compiler: gcc 22 | cpp_compiler: g++ 23 | - os: ubuntu-latest 24 | c_compiler: clang 25 | cpp_compiler: clang++ 26 | - os: macos-latest 27 | c_compiler: clang 28 | cpp_compiler: clang++ 29 | exclude: 30 | - os: ubuntu-latest 31 | c_compiler: cl 32 | - os: macos-latest 33 | c_compiler: gcc 34 | - os: macos-latest 35 | c_compiler: cl 36 | 37 | steps: 38 | - uses: actions/checkout@v3 39 | 40 | # -------- 安装 SDL2 依赖 -------- 41 | - name: Install SDL2 and build tools (Ubuntu) 42 | if: matrix.os == 'ubuntu-latest' 43 | run: | 44 | sudo apt-get update 45 | sudo apt-get install -y build-essential cmake libsdl2-dev 46 | 47 | - name: Install SDL2 and build tools (macOS) 48 | if: matrix.os == 'macos-latest' 49 | run: | 50 | brew update 51 | brew install sdl2 cmake make 52 | 53 | - name: Install SDL2 and build tools (Windows) 54 | if: matrix.os == 'windows-latest' 55 | run: | 56 | git clone https://github.com/microsoft/vcpkg.git 57 | .\vcpkg\bootstrap-vcpkg.bat 58 | .\vcpkg\vcpkg install sdl2 59 | 60 | # -------- 构建目录变量 -------- 61 | - name: Set reusable strings 62 | id: strings 63 | shell: bash 64 | run: | 65 | echo "build-output-dir=${{ github.workspace }}/build" >> "$GITHUB_OUTPUT" 66 | 67 | # -------- CMake 配置 -------- 68 | - name: Configure CMake (Windows) 69 | if: matrix.os == 'windows-latest' 70 | run: > 71 | cmake -B ${{ steps.strings.outputs.build-output-dir }} 72 | -DCMAKE_CXX_COMPILER=${{ matrix.cpp_compiler }} 73 | -DCMAKE_C_COMPILER=${{ matrix.c_compiler }} 74 | -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} 75 | -DCMAKE_TOOLCHAIN_FILE=${{ github.workspace }}/vcpkg/scripts/buildsystems/vcpkg.cmake 76 | -S ${{ github.workspace }} 77 | 78 | - name: Configure CMake (Unix) 79 | if: matrix.os != 'windows-latest' 80 | run: > 81 | cmake -B ${{ steps.strings.outputs.build-output-dir }} 82 | -DCMAKE_CXX_COMPILER=${{ matrix.cpp_compiler }} 83 | -DCMAKE_C_COMPILER=${{ matrix.c_compiler }} 84 | -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} 85 | -S ${{ github.workspace }} 86 | 87 | # -------- 构建项目 -------- 88 | - name: Build 89 | run: cmake --build ${{ steps.strings.outputs.build-output-dir }} --config ${{ matrix.build_type }} 90 | 91 | # -------- 运行测试 -------- 92 | - name: Test 93 | working-directory: ${{ steps.strings.outputs.build-output-dir }} 94 | run: ctest --build-config ${{ matrix.build_type }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | .vscode 3 | .cache 4 | .DS_Store 5 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.10) 2 | 3 | set(SMOOTH_UI_TOOLKIT_ROOT_DIR ${CMAKE_CURRENT_LIST_DIR}) 4 | 5 | if (ESP_PLATFORM) 6 | include(${CMAKE_CURRENT_LIST_DIR}/cmake/esp_idf.cmake) 7 | else() 8 | include(${CMAKE_CURRENT_LIST_DIR}/cmake/basic.cmake) 9 | endif() -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Forairaaaaa 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Smooth UI Toolkit 2 | 3 | C++ UI 动画工具集 4 | 5 | - Spring 动画、Easing 动画插值 6 | - Lvgl C++ 封装 7 | - signal、ringbuffer ... 8 | 9 | ![](https://pic1.imgdb.cn/item/680c639c58cb8da5c8ce22d2.gif) 10 | 11 | ![](https://pic1.imgdb.cn/item/680c58b558cb8da5c8ce1f5b.gif) 12 | 13 | ![](https://pic1.imgdb.cn/item/680c58b458cb8da5c8ce1f5a.gif) 14 | 15 | ## Animate 16 | 17 | 基础动画插值类,可配置起止、循环方式、次数、动画类型等,默认动画类型为 **spring** 18 | 19 | 插值类抽象深受 [Motion](https://motion.dev/) 启发,多谢 Motion 哥 20 | 21 | ![](https://pic1.imgdb.cn/item/680c58b458cb8da5c8ce1f57.gif) 22 | 23 | ```cpp 24 | Animate animation; 25 | 26 | animation.start = 200; 27 | animation.end = 600; 28 | animation.repeat = -1; 29 | animation.repeatType = animate_repeat_type::reverse; 30 | 31 | // 这里调用 spring option ,则动画类型为 spring 32 | animation.springOptions().bounce = 0.4; 33 | animation.springOptions().visualDuration = 0.6; 34 | 35 | // 如想要 easing 动画,调用 easing option 即可 36 | // animation.easingOptions().easingFunction = ease::ease_out_quad; 37 | // animation.easingOptions().duration = 0.3; 38 | 39 | animation.init(); 40 | animation.play(); 41 | 42 | while (1) { 43 | // 更新 44 | animation.update(); 45 | // 取值 46 | draw_ball(animation.value(), 233); 47 | } 48 | ``` 49 | 50 | ## AnimateValue 51 | 52 | Animate 的派生类,大幅简化赋值、取值操作 53 | 54 | 适合控件坐标、长宽等参数的快速动画化: 55 | 56 | 视频:[介绍](https://www.bilibili.com/video/BV1RZcTegEUu) 57 | 58 | ![](https://pic1.imgdb.cn/item/680c58b458cb8da5c8ce1f58.gif) 59 | 60 | ```cpp 61 | AnimateValue x = 100; 62 | AnimateValue y = 225; 63 | 64 | while (1) { 65 | // 赋值时自动适应新目标 66 | x = get_mouse_x(); 67 | y = get_mouse_y(); 68 | 69 | // 取值时自动更新、类型转换 70 | draw_ball(x, y); 71 | }); 72 | ``` 73 | 74 | ## Lvgl Cpp 75 | 76 | 再见吧👋 lv_obj_del,可以用智能指针来管理 lvgl 控件了 77 | 78 | 指针管理参考:*https://github.com/vpaeder/lvglpp* 79 | 80 | 用了类似 Godot Signal 的信号槽来简化原来的 event 回调 81 | 82 | ![](https://pic1.imgdb.cn/item/680c58b458cb8da5c8ce1f59.gif) 83 | 84 | ```cpp 85 | #include 86 | // lvgl cpp 封装为 header only 87 | // 需要工程已满足 #include 依赖 88 | // 当前目标版本为 v9.2.2 89 | 90 | // Basic lvgl object 91 | auto obj = new Container(lv_screen_active()); 92 | obj->setPos(50, 50); 93 | obj->setSize(200, 100); 94 | 95 | // Label 96 | auto label = new Label(lv_screen_active()); 97 | label->setTextFont(&lv_font_montserrat_24); 98 | label->setAlign(LV_ALIGN_CENTER); 99 | label->setText("????????????"); 100 | 101 | // Button 102 | int count = 0; 103 | auto btn = new Button(lv_screen_active()); 104 | btn->setPos(50, 200); 105 | btn->label().setText("+1"); 106 | btn->onClick().connect([&]() { 107 | label->setText(fmt::format("{}", count++)); 108 | }); 109 | 110 | // Switch 111 | auto sw = new Switch(lv_screen_active()); 112 | sw->setPos(50, 300); 113 | sw->onValueChanged().connect([&](bool value) { 114 | label->setText(value ? "ON" : "OFF"); 115 | }); 116 | 117 | // Slider 118 | auto slider = new Slider(lv_screen_active()); 119 | slider->setPos(50, 400); 120 | slider->onValueChanged().connect([&](int value) { 121 | label->setText(fmt::format("{}", value)); 122 | }); 123 | ``` 124 | 125 | ## UI HAL 126 | 127 | 动画的更新以系统时间为参考基准,所使用的相关函数来自内部定义: 128 | 129 | ```cpp 130 | namespace ui_hal { 131 | 132 | /** 133 | * @brief Get the number of milliseconds since running 134 | * 135 | * @return uint32_t 136 | */ 137 | uint32_t get_tick(); 138 | 139 | /** 140 | * @brief Wait a specified number of milliseconds before returning 141 | * 142 | * @param ms 143 | */ 144 | void delay(uint32_t ms); 145 | 146 | } // namespace ui_hal 147 | ``` 148 | 149 | 其默认实现为 cpp chrono 库 150 | 151 | 如有需求,可自定义实现方式: 152 | 153 | ```cpp 154 | // Arduino 为例,性能应该比 chrono 好 155 | 156 | ui_hal::on_get_tick_ms([]() { 157 | return millis(); 158 | }); 159 | 160 | ui_hal::on_delay([](uint32_t ms) { 161 | delay(ms); 162 | }); 163 | ``` 164 | 165 | ## 编译例程 166 | 167 | 例程里用到了 [lvgl](https://github.com/lvgl/lvgl) 和 [raylib](https://github.com/raysan5/raylib) 作为图形库,所以要安装 [SDL2](https://github.com/libsdl-org/SDL) 168 | 169 | ### 工具链安装: 170 | 171 | - **macOS:** `brew install sdl2 cmake make` 172 | - **Ubuntu:** `sudo apt install build-essential cmake libsdl2-dev` 173 | 174 | ### 拉取项目: 175 | 176 | ```bash 177 | git clone https://github.com/Forairaaaaa/smooth_ui_toolkit.git 178 | ``` 179 | 180 | ### 编译: 181 | 182 | ```bash 183 | cd smooth_ui_toolkit && mkdir build 184 | ``` 185 | 186 | ```bash 187 | cd build && cmake .. && make -j8 188 | ``` 189 | 190 | cmake 过程过程中会拉取依赖 git 仓库,确保网络正常访问 191 | 192 | ## 库引入 193 | 194 | ### CMake工程 195 | 196 | 工程 `CMakeLists.txt` 里添加: 197 | 198 | ```cmake 199 | # 不编译例程 200 | set(SMOOTH_UI_TOOLKIT_BUILD_EXAMPLE OFF) 201 | 202 | # 引入库路径 203 | add_subdirectory(path/to/smooth_ui_toolkit) 204 | 205 | # link 206 | target_link_libraries(your_project PUBLIC 207 | smooth_ui_toolkit 208 | ) 209 | ``` 210 | 211 | ### IDF 工程 212 | 213 | clone 仓库,直接丢到 `components` 目录里就行 214 | 215 | ### PIO 工程 216 | 217 | clone 仓库,直接丢到 `libs` 目录里就行 218 | 219 | ### Arduino 工程 220 | 221 | clone 仓库,直接丢到 `xxx` 目录里就行(我不记得那个 library 目录叫什么了) 222 | 223 | ## TODO 224 | 225 | - [ ] ui_hal 的内部 cpp 实现添加宏定义,避免在自定义实现时多余的 linkage 226 | - [ ] NumberFlow 类在 linux 上有 bug,DigitFlow 正常 227 | - [ ] AnimateVlaue 如果设置了 delay,只会在第一次有效,retarget 后无效 228 | - [ ] 脚本化 lvgl widget api 封装 229 | 230 | -------------------------------------------------------------------------------- /cmake/basic.cmake: -------------------------------------------------------------------------------- 1 | project(smooth_ui_toolkit) 2 | 3 | set(CMAKE_CXX_STANDARD 17) 4 | cmake_policy(SET CMP0077 NEW) 5 | 6 | # Src files 7 | file(GLOB_RECURSE SMOOTH_UI_TOOLKIT_SRCS 8 | ${SMOOTH_UI_TOOLKIT_ROOT_DIR}/src/*.c 9 | ${SMOOTH_UI_TOOLKIT_ROOT_DIR}/src/*.cc 10 | ${SMOOTH_UI_TOOLKIT_ROOT_DIR}/src/*.cpp 11 | ) 12 | # Include 13 | set(SMOOTH_UI_TOOLKIT_INCS 14 | ${SMOOTH_UI_TOOLKIT_ROOT_DIR}/src/ 15 | ${SMOOTH_UI_TOOLKIT_ROOT_DIR}/src/lvgl/ 16 | ) 17 | 18 | 19 | add_library(${PROJECT_NAME} ${SMOOTH_UI_TOOLKIT_SRCS}) 20 | target_include_directories(${PROJECT_NAME} PUBLIC ${SMOOTH_UI_TOOLKIT_INCS}) 21 | 22 | 23 | # Example 24 | option(SMOOTH_UI_TOOLKIT_BUILD_EXAMPLE "Build example" ON) 25 | if(SMOOTH_UI_TOOLKIT_BUILD_EXAMPLE) 26 | add_subdirectory(./example/) 27 | endif() 28 | 29 | 30 | # CTest 31 | option(SMOOTH_UI_TOOLKIT_BUILD_TEST "Build test" ON) 32 | if(SMOOTH_UI_TOOLKIT_BUILD_TEST) 33 | add_subdirectory(./test/) 34 | enable_testing() 35 | add_test(ringbuffer test/ringbuffer_test) 36 | endif() -------------------------------------------------------------------------------- /cmake/esp_idf.cmake: -------------------------------------------------------------------------------- 1 | # Src files 2 | file(GLOB_RECURSE SMOOTH_UI_TOOLKIT_SRCS 3 | ${SMOOTH_UI_TOOLKIT_ROOT_DIR}/src/*.c 4 | ${SMOOTH_UI_TOOLKIT_ROOT_DIR}/src/*.cc 5 | ${SMOOTH_UI_TOOLKIT_ROOT_DIR}/src/*.cpp 6 | ) 7 | # Include 8 | set(SMOOTH_UI_TOOLKIT_INCS 9 | ${SMOOTH_UI_TOOLKIT_ROOT_DIR}/src/ 10 | ${SMOOTH_UI_TOOLKIT_ROOT_DIR}/src/lvgl/ 11 | ) 12 | 13 | # Public component requirement 14 | set(SMOOTH_UI_TOOLKIT_REQUIRES 15 | ) 16 | 17 | # Private component requirement 18 | set(SMOOTH_UI_TOOLKIT_PRIV_REQUIRES 19 | ) 20 | 21 | # Register component 22 | idf_component_register( 23 | SRCS ${SMOOTH_UI_TOOLKIT_SRCS} 24 | INCLUDE_DIRS ${SMOOTH_UI_TOOLKIT_INCS} 25 | REQUIRES ${SMOOTH_UI_TOOLKIT_REQUIRES} 26 | PRIV_REQUIRES ${SMOOTH_UI_TOOLKIT_PRIV_REQUIRES} 27 | ) -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | dependencies 2 | -------------------------------------------------------------------------------- /example/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | include(FetchContent) 2 | 3 | # Dependencies 4 | FetchContent_Declare( 5 | raylib 6 | GIT_REPOSITORY https://github.com/raysan5/raylib.git 7 | GIT_TAG 5.5 8 | ) 9 | FetchContent_MakeAvailable(raylib) 10 | 11 | set(MOONCAKE_LOG_BUILD_EXAMPLE OFF) 12 | FetchContent_Declare( 13 | mooncake_log 14 | GIT_REPOSITORY https://github.com/Forairaaaaa/mooncake_log.git 15 | GIT_TAG main 16 | ) 17 | FetchContent_MakeAvailable(mooncake_log) 18 | 19 | set(LV_CONF_INCLUDE_SIMPLE OFF) 20 | set(LV_CONF_PATH ${CMAKE_SOURCE_DIR}/example/utils/lv_conf.h) 21 | FetchContent_Declare( 22 | lvgl 23 | GIT_REPOSITORY https://github.com/lvgl/lvgl.git 24 | GIT_TAG v9.2.2 25 | ) 26 | FetchContent_MakeAvailable(lvgl) 27 | 28 | find_package(SDL2 REQUIRED SDL2) 29 | include_directories(PUBLIC ${SDL2_INCLUDE_DIRS}) 30 | target_include_directories(lvgl PUBLIC ${SDL2_INCLUDE_DIRS}) 31 | 32 | add_executable(dev_zone ./dev_zone.cpp) 33 | target_link_libraries(dev_zone ${PROJECT_NAME} raylib mooncake_log lvgl lvgl_examples lvgl_demos ${SDL2_LIBRARIES}) 34 | 35 | add_executable(ui_hal ./basic/ui_hal.cpp) 36 | target_link_libraries(ui_hal ${PROJECT_NAME} raylib mooncake_log) 37 | 38 | add_executable(animate ./basic/animate.cpp) 39 | target_link_libraries(animate ${PROJECT_NAME} raylib mooncake_log) 40 | 41 | add_executable(animate_value ./basic/animate_value.cpp) 42 | target_link_libraries(animate_value ${PROJECT_NAME} raylib mooncake_log) 43 | 44 | add_executable(animate_value_options ./basic/animate_value_options.cpp) 45 | target_link_libraries(animate_value_options ${PROJECT_NAME} raylib mooncake_log) 46 | 47 | add_executable(basic_animations ./basic/basic_animations.cpp) 48 | target_link_libraries(basic_animations ${PROJECT_NAME} mooncake_log lvgl ${SDL2_LIBRARIES}) 49 | 50 | add_executable(multi_cursor ./basic/multi_cursor.cpp) 51 | target_link_libraries(multi_cursor ${PROJECT_NAME} raylib mooncake_log) 52 | 53 | add_executable(bubbles ./basic/bubbles.cpp) 54 | target_link_libraries(bubbles ${PROJECT_NAME} raylib mooncake_log) 55 | 56 | add_executable(event_queue ./event/event_queue.cpp) 57 | target_link_libraries(event_queue ${PROJECT_NAME} pthread) 58 | 59 | add_executable(signal ./event/signal.cpp) 60 | target_link_libraries(signal ${PROJECT_NAME} pthread) 61 | 62 | add_executable(number_flow ./smooth_lvgl/number_flow.cpp) 63 | target_link_libraries(number_flow ${PROJECT_NAME} mooncake_log lvgl ${SDL2_LIBRARIES}) 64 | 65 | add_executable(lvgl_cpp ./smooth_lvgl/lvgl_cpp.cpp) 66 | target_link_libraries(lvgl_cpp ${PROJECT_NAME} mooncake_log lvgl ${SDL2_LIBRARIES}) 67 | 68 | add_executable(animate_color ./basic/animate_color.cpp) 69 | target_link_libraries(animate_color ${PROJECT_NAME} mooncake_log lvgl ${SDL2_LIBRARIES}) 70 | 71 | add_executable(animte_sequence ./basic/animte_sequence.cpp) 72 | target_link_libraries(animte_sequence ${PROJECT_NAME} mooncake_log lvgl ${SDL2_LIBRARIES}) 73 | -------------------------------------------------------------------------------- /example/basic/animate.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file animate.cpp 3 | * @author Forairaaaaa 4 | * @brief 5 | * @version 0.1 6 | * @date 2025-01-08 7 | * 8 | * @copyright Copyright (c) 2025 9 | * 10 | */ 11 | #include "../utils/raylib_wrapper.h" 12 | #include 13 | #include 14 | 15 | using namespace smooth_ui_toolkit; 16 | 17 | int main() 18 | { 19 | Animate animation; 20 | 21 | // Animation options 22 | // Refs: https://motion.dev/docs/animate#options 23 | animation.start = 200; 24 | animation.end = 600; 25 | animation.repeat = -1; 26 | animation.repeatType = animate_repeat_type::reverse; 27 | animation.springOptions().bounce = 0.4; 28 | animation.springOptions().visualDuration = 0.6; 29 | 30 | // Callbacks 31 | animation.onUpdate([&](const float& value) { mclog::info("{}", value); }); 32 | animation.onComplete([&]() { mclog::info("done"); }); 33 | 34 | // Init and play 35 | animation.init(); 36 | animation.play(); 37 | 38 | raylib::create_window(800, 450, "你好", [&]() { 39 | // Update animation 40 | animation.update(); 41 | 42 | // Render 43 | ClearBackground(BLACK); 44 | DrawCircle(animation.value(), 225, 30, LIGHTGRAY); 45 | }); 46 | 47 | return 0; 48 | } 49 | -------------------------------------------------------------------------------- /example/basic/animate_color.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file animate_color.cpp 3 | * @author Forairaaaaa 4 | * @brief 5 | * @version 0.1 6 | * @date 2025-04-04 7 | * 8 | * @copyright Copyright (c) 2025 9 | * 10 | */ 11 | #include "../utils/lvgl_wrapper.h" 12 | #include 13 | #include 14 | #include 15 | #include 16 | 17 | using namespace smooth_ui_toolkit; 18 | using namespace smooth_ui_toolkit::lvgl_cpp; 19 | using namespace smooth_ui_toolkit::color; 20 | 21 | int main() 22 | { 23 | lvgl::create_window(800, 520); 24 | 25 | std::vector color_list = {0x9FB3DF, 0xD4C9BE, 0x9ACBD0, 0xF2EFE7, 0x8AB2A6, 0xACD3A8, 0xFFEDFA, 26 | 0xB7B1F2, 0xEBE5C2, 0xB9B28A, 0xFFD95F, 0xA6F1E0, 0xFFEDFA, 0xEAFAEA, 27 | 0xC8AAAA, 0x16C47F, 0xFAFFC5, 0xFF8383, 0xFFF574, 0xFCFFC1}; 28 | 29 | // Color animation 30 | AnimateRgb_t bg_color; 31 | bg_color.duration = 0.3; 32 | bg_color.begin(); 33 | bg_color = 0xffffff; 34 | 35 | // Color label 36 | auto label = Label(lv_screen_active()); 37 | label.setAlign(LV_ALIGN_CENTER); 38 | label.setPos(0, -50); 39 | label.setTextFont(&lv_font_montserrat_24); 40 | label.setText("click me"); 41 | 42 | // Switch color when label is clicked 43 | label.addFlag(LV_OBJ_FLAG_CLICKABLE); 44 | label.onClick().connect([&]() { 45 | std::random_device rd; 46 | std::mt19937 gen(rd()); 47 | std::uniform_int_distribution<> dis(0, color_list.size() - 1); 48 | bg_color = color_list[dis(gen)]; 49 | label.setText(bg_color.toHexString()); 50 | }); 51 | 52 | while (1) { 53 | // Update color animation 54 | bg_color.update(); 55 | 56 | // Apply color 57 | lv_obj_set_style_bg_color(lv_screen_active(), lv_color_hex(bg_color.toHex()), LV_PART_MAIN); 58 | label.setTextColor(lv_color_hex(blend_in_difference(bg_color, 0xaaaaaa).toHex())); 59 | 60 | lvgl::update_window(); 61 | } 62 | 63 | return 0; 64 | } 65 | -------------------------------------------------------------------------------- /example/basic/animate_value.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file animate_value.cpp 3 | * @author Forairaaaaa 4 | * @brief 5 | * @version 0.1 6 | * @date 2025-01-08 7 | * 8 | * @copyright Copyright (c) 2025 9 | * 10 | */ 11 | #include "../utils/raylib_wrapper.h" 12 | #include 13 | #include 14 | 15 | using namespace smooth_ui_toolkit; 16 | 17 | int main() 18 | { 19 | // Default value 20 | AnimateValue x = 100; 21 | AnimateValue y = 225; 22 | 23 | raylib::create_window(800, 450, "你好", [&]() { 24 | // Update new position on mouse click 25 | if (IsMouseButtonDown(MOUSE_BUTTON_LEFT)) { 26 | mclog::info("new postion ({}, {})", GetMouseX(), GetMouseY()); 27 | x = GetMouseX(); 28 | y = GetMouseY(); 29 | } 30 | 31 | // Render 32 | ClearBackground(BLACK); 33 | DrawText("Click To Move The Ball", 280, 200, 20, DARKGRAY); 34 | DrawCircle(x, y, 30, LIGHTGRAY); 35 | }); 36 | 37 | return 0; 38 | } 39 | -------------------------------------------------------------------------------- /example/basic/animate_value_options.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file animate_value_options.cpp 3 | * @author Forairaaaaa 4 | * @brief 5 | * @version 0.1 6 | * @date 2025-01-10 7 | * 8 | * @copyright Copyright (c) 2025 9 | * 10 | */ 11 | #include "../utils/raylib_wrapper.h" 12 | #include 13 | #include 14 | 15 | using namespace smooth_ui_toolkit; 16 | 17 | int main() 18 | { 19 | // Default value 20 | AnimateValue x = 100; 21 | AnimateValue y = 225; 22 | 23 | // Stop animation before option setting 24 | x.stop(); 25 | y.stop(); 26 | 27 | // Spring options 28 | x.springOptions().bounce = 0.1; 29 | y.springOptions().bounce = 0.1; 30 | x.springOptions().visualDuration = 0.6; 31 | y.springOptions().visualDuration = 0.6; 32 | 33 | // Or easing options 34 | // x.easingOptions().easingFunction = ease::ease_out_quad; 35 | // y.easingOptions().easingFunction = ease::ease_out_quad; 36 | // x.easingOptions().duration = 0.3; 37 | // y.easingOptions().duration = 0.3; 38 | 39 | // Begin animation 40 | x.begin(); 41 | y.begin(); 42 | 43 | raylib::create_window(800, 450, "你好", [&]() { 44 | // Update new position on mouse click 45 | if (IsMouseButtonDown(MOUSE_BUTTON_LEFT)) { 46 | mclog::info("new postion ({}, {})", GetMouseX(), GetMouseY()); 47 | x = GetMouseX(); 48 | y = GetMouseY(); 49 | } 50 | 51 | // Render 52 | ClearBackground(BLACK); 53 | DrawText("Click To Move The Ball", 280, 200, 20, DARKGRAY); 54 | DrawCircle(x, y, 30, LIGHTGRAY); 55 | }); 56 | 57 | return 0; 58 | } 59 | -------------------------------------------------------------------------------- /example/basic/animte_sequence.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file animte_sequence.cpp 3 | * @author Forairaaaaa 4 | * @brief 5 | * @version 0.1 6 | * @date 2025-04-04 7 | * 8 | * @copyright Copyright (c) 2025 9 | * 10 | */ 11 | #include "../utils/lvgl_wrapper.h" 12 | #include 13 | #include 14 | #include 15 | 16 | using namespace smooth_ui_toolkit; 17 | using namespace smooth_ui_toolkit::lvgl_cpp; 18 | 19 | std::vector generate_damping_sine_sequence(double start_amp, double end_amp, int num_extremes) 20 | { 21 | std::vector extremes; 22 | 23 | // Start value 24 | extremes.push_back(0); 25 | 26 | for (int i = 0; i < num_extremes; ++i) { 27 | double t_ratio = static_cast(i) / (num_extremes - 1); // 归一化时间 (0~1) 28 | double amp = start_amp + (end_amp - start_amp) * t_ratio; // 线性衰减 29 | extremes.push_back((i % 2 == 0) ? amp : -amp); // 交替存储波峰和波谷 30 | } 31 | 32 | return extremes; 33 | } 34 | 35 | int main() 36 | { 37 | lvgl::create_window(800, 520); 38 | 39 | auto shake_box = Container(lv_screen_active()); 40 | shake_box.setAlign(LV_ALIGN_CENTER); 41 | 42 | auto shake_anim = AnimateSequence(); 43 | shake_anim.setSequence(generate_damping_sine_sequence(100, 0, 20)); 44 | shake_anim.repeat = -1; 45 | shake_anim.onStep([&](AnimateValue& animateValue, std::vector& values, int step) { 46 | // Setup animation options on callback 47 | animateValue.delay = (step == 1) ? 1 : 0; 48 | animateValue.easingOptions().duration = 0.12; 49 | }); 50 | shake_anim.play(); 51 | 52 | while (1) { 53 | // Apply animation 54 | shake_box.setPos(shake_anim, -20); 55 | 56 | lvgl::update_window(); 57 | } 58 | 59 | return 0; 60 | } 61 | -------------------------------------------------------------------------------- /example/basic/basic_animations.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file basic_animations.cpp 3 | * @author Forairaaaaa 4 | * @brief 5 | * @version 0.1 6 | * @date 2025-01-10 7 | * 8 | * @copyright Copyright (c) 2025 9 | * 10 | */ 11 | // Refs: https://motion.dev/docs/react-animation#basic-animations 12 | #include "../utils/lvgl_wrapper.h" 13 | #include 14 | #include 15 | 16 | using namespace smooth_ui_toolkit; 17 | 18 | struct Position { 19 | AnimateValue x = 0; 20 | AnimateValue y = 0; 21 | AnimateValue rotate = 0; 22 | }; 23 | 24 | int main() 25 | { 26 | lvgl::create_window(800, 520); 27 | 28 | // Set box default position 29 | const int box_default_x = -220; 30 | const int box_default_y = 0; 31 | const int box_default_rotate = 0; 32 | 33 | // Create box postion buffer 34 | Position box_position = {box_default_x, box_default_y, box_default_rotate}; 35 | 36 | // Create a box 37 | lvgl::Box box(250, 250); 38 | 39 | // Create postion sliders 40 | lvgl::Slider slider_x("x", -200, 200); 41 | slider_x.move(180, -100); 42 | slider_x.onValueChanged([&](int value) { 43 | box_position.x = box_default_x + value; 44 | mclog::info("move box x to {}", value); 45 | }); 46 | 47 | lvgl::Slider slider_y("y", -200, 200); 48 | slider_y.move(180, 0); 49 | slider_y.onValueChanged([&](int value) { 50 | box_position.y = box_default_y + value; 51 | mclog::info("move box y to {}", value); 52 | }); 53 | 54 | lvgl::Slider slider_rotate("rotate", -180, 180); 55 | slider_rotate.move(180, 100); 56 | slider_rotate.onValueChanged([&](int value) { 57 | box_position.rotate = box_default_rotate + value; 58 | mclog::info("move box rotate to {}", value); 59 | }); 60 | 61 | // Create reset button 62 | lvgl::Button button("reset"); 63 | button.move(300, -200); 64 | button.onClick([&]() { 65 | box_position.x = box_default_x; 66 | box_position.y = box_default_y; 67 | box_position.rotate = box_default_rotate; 68 | slider_x.setValue(0); 69 | slider_y.setValue(0); 70 | slider_rotate.setValue(0); 71 | mclog::info("reset box position"); 72 | }); 73 | 74 | // Lvgl loop 75 | while (1) { 76 | // Keep moving the box to the new position 77 | box.move(box_position.x, box_position.y, box_position.rotate); 78 | lvgl::update_window(); 79 | } 80 | 81 | return 0; 82 | } 83 | -------------------------------------------------------------------------------- /example/basic/bubbles.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file bubbles.cpp 3 | * @author Forairaaaaa 4 | * @brief 5 | * @version 0.1 6 | * @date 2025-01-10 7 | * 8 | * @copyright Copyright (c) 2025 9 | * 10 | */ 11 | #include "../utils/raylib_wrapper.h" 12 | #include 13 | #include 14 | #include 15 | 16 | using namespace smooth_ui_toolkit; 17 | 18 | struct Bubble { 19 | AnimateValue x; 20 | AnimateValue y; 21 | int radius; 22 | Color color; 23 | }; 24 | 25 | int main() 26 | { 27 | std::vector bubbles; 28 | 29 | raylib::create_window( 30 | 800, 450, "你好", 31 | [&]() { 32 | // Move to mouse postion when click 33 | if (IsMouseButtonDown(MOUSE_BUTTON_LEFT)) { 34 | for (auto& bubble : bubbles) { 35 | bubble.x = GetMouseX(); 36 | bubble.y = GetMouseY(); 37 | } 38 | } else if (IsMouseButtonReleased(MOUSE_BUTTON_LEFT)) { 39 | for (auto& bubble : bubbles) { 40 | bubble.x = GetRandomValue(0, GetScreenWidth()); 41 | bubble.y = GetRandomValue(0, GetScreenHeight()); 42 | } 43 | } 44 | 45 | // Render 46 | ClearBackground(BLACK); 47 | for (int i = 0; i < bubbles.size(); i++) { 48 | DrawCircle(bubbles[i].x, bubbles[i].y, bubbles[i].radius, bubbles[i].color); 49 | } 50 | }, 51 | [&]() { 52 | // Generate random bubbles 53 | for (int i = 0; i < 1145; i++) { 54 | bubbles.push_back({ 55 | (int)(GetScreenWidth() / 2), 56 | (int)(GetScreenHeight() / 2), 57 | GetRandomValue(3, 6), 58 | GetRandomColor(), 59 | }); 60 | bubbles.back().x.springOptions().stiffness = GetRandomValue(50, 150); 61 | bubbles.back().x.springOptions().damping = GetRandomValue(5, 15); 62 | bubbles.back().y.springOptions() = bubbles.back().x.springOptions(); 63 | bubbles.back().x = GetRandomValue(0, GetScreenWidth()); 64 | bubbles.back().y = GetRandomValue(0, GetScreenHeight()); 65 | } 66 | }); 67 | 68 | return 0; 69 | } 70 | -------------------------------------------------------------------------------- /example/basic/multi_cursor.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file multi_cursor.cpp 3 | * @author Forairaaaaa 4 | * @brief 5 | * @version 0.1 6 | * @date 2025-01-10 7 | * 8 | * @copyright Copyright (c) 2025 9 | * 10 | */ 11 | // Refs: https://motion.dev/docs/react-animation bottom Motion+: "Introducing Cursor" 12 | #include "../utils/raylib_wrapper.h" 13 | #include "raylib.h" 14 | #include 15 | #include 16 | #include 17 | 18 | using namespace smooth_ui_toolkit; 19 | 20 | struct MyVector2D { 21 | AnimateValue x; 22 | AnimateValue y; 23 | }; 24 | 25 | void update_cursors(int mouseX, int mouseY, std::vector& cursors) 26 | { 27 | const int radius = 55; 28 | const float angleStep = 60.0f; 29 | for (int i = 0; i < 6; ++i) { 30 | float angle = angleStep * i * (M_PI / 180.0f); 31 | int dx = static_cast(radius * cos(angle)); 32 | int dy = static_cast(radius * sin(angle)); 33 | cursors[i].x = mouseX + dx; 34 | cursors[i].y = mouseY + dy; 35 | } 36 | } 37 | 38 | int main() 39 | { 40 | // Create cursors 41 | std::vector cursors(6); 42 | // Create cursor colors 43 | std::vector colors = {0xFF0088FF, 0xDD00EEFF, 0x9911FFFF, 0x7700FFFF, 0x4400FFFF, 0x0D63F8FF}; 44 | 45 | // Setup cursors animation options 46 | for (int i = 0; i < cursors.size(); i++) { 47 | cursors[i].x.stop(); 48 | cursors[i].y.stop(); 49 | cursors[i].x.springOptions().stiffness = 55 + i * 25; 50 | cursors[i].x.springOptions().damping = 13 - i; 51 | cursors[i].y.springOptions() = cursors[i].x.springOptions(); 52 | cursors[i].x.begin(); 53 | cursors[i].y.begin(); 54 | } 55 | 56 | raylib::create_window( 57 | 800, 450, "你好", 58 | [&]() { 59 | // If mouse inside of window, move to mouse position 60 | if (CheckCollisionPointRec(GetMousePosition(), (Rectangle){0, 0, 800, 450})) { 61 | update_cursors(GetMouseX(), GetMouseY(), cursors); 62 | } 63 | // If not, move back to center 64 | else { 65 | update_cursors(GetScreenWidth() / 2, GetScreenHeight() / 2, cursors); 66 | } 67 | 68 | // Render 69 | ClearBackground(GetColor(0x181B1F)); 70 | DrawCircle(GetMouseX(), GetMouseY(), 8, WHITE); 71 | for (int i = 0; i < cursors.size(); i++) { 72 | DrawCircle(cursors[i].x, cursors[i].y, 8, GetColor(colors[i])); 73 | } 74 | }, 75 | []() { HideCursor(); }); 76 | 77 | return 0; 78 | } 79 | -------------------------------------------------------------------------------- /example/basic/ui_hal.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file ui_hal.cpp 3 | * @author Forairaaaaa 4 | * @brief 5 | * @version 0.1 6 | * @date 2025-01-07 7 | * 8 | * @copyright Copyright (c) 2025 9 | * 10 | */ 11 | #include 12 | #include 13 | 14 | using namespace smooth_ui_toolkit; 15 | 16 | int main() 17 | { 18 | // Custom on tick callback 19 | // ui_hal::on_get_tick_ms([]() { 20 | // return millis(); 21 | // return SDL_GetTicks(); 22 | // }); 23 | 24 | // Custom on delay callback 25 | // ui_hal::on_delay([](uint32_t ms) { 26 | // delay(ms); 27 | // SDL_Delay(ms); 28 | // }); 29 | 30 | while (1) { 31 | mclog::info("{}ms {}s", ui_hal::get_tick(), ui_hal::get_tick_s()); 32 | ui_hal::delay(500); 33 | mclog::info("{}ms {}s", ui_hal::get_tick(), ui_hal::get_tick_s()); 34 | ui_hal::delay_s(0.5); 35 | } 36 | return 0; 37 | } 38 | -------------------------------------------------------------------------------- /example/dev_zone.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file dev_zone.cpp 3 | * @author Forairaaaaa 4 | * @brief 5 | * @version 0.1 6 | * @date 2025-01-07 7 | * 8 | * @copyright Copyright (c) 2025 9 | * 10 | */ 11 | // git update-index --skip-worktree example/dev_zone.cpp 12 | #include "utils/lvgl_wrapper.h" 13 | #include "utils/raylib_wrapper.h" 14 | #include 15 | #include 16 | #include 17 | 18 | using namespace smooth_ui_toolkit; 19 | using namespace smooth_ui_toolkit::lvgl_cpp; 20 | 21 | static void raylib_window() 22 | { 23 | raylib::create_window(800, 450, "你好", [&]() { 24 | // Render 25 | ClearBackground(RAYWHITE); 26 | }); 27 | } 28 | 29 | static void lvgl_window() 30 | { 31 | lvgl::create_window(800, 520); 32 | 33 | while (1) { 34 | lvgl::update_window(); 35 | } 36 | } 37 | 38 | int main() 39 | { 40 | raylib_window(); 41 | // lvgl_window(); 42 | return 0; 43 | } -------------------------------------------------------------------------------- /example/event/event_queue.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file event_queue.cpp 3 | * @author Forairaaaaa 4 | * @brief 5 | * @version 0.1 6 | * @date 2025-03-21 7 | * 8 | * @copyright Copyright (c) 2025 9 | * 10 | */ 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | 17 | using namespace smooth_ui_toolkit; 18 | 19 | int main() 20 | { 21 | EventQueue event_queue; 22 | 23 | std::thread producer([&]() { 24 | for (int i = 1; i < 6; i++) { 25 | event_queue.emit(i); 26 | std::this_thread::sleep_for(std::chrono::seconds(1)); 27 | } 28 | }); 29 | 30 | std::thread consumer([&]() { 31 | bool is_running = true; 32 | while (is_running) { 33 | event_queue.poll([&](int event) { 34 | std::cout << "on event: " << event << std::endl; 35 | if (event == 5) { 36 | is_running = false; 37 | } 38 | }); 39 | } 40 | }); 41 | 42 | producer.join(); 43 | consumer.join(); 44 | return 0; 45 | } -------------------------------------------------------------------------------- /example/event/signal.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file signal.cpp 3 | * @author Forairaaaaa 4 | * @brief 5 | * @version 0.1 6 | * @date 2025-03-21 7 | * 8 | * @copyright Copyright (c) 2025 9 | * 10 | */ 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | 17 | using namespace smooth_ui_toolkit; 18 | 19 | void consumer(int id, Signal& signal) 20 | { 21 | bool is_running = true; 22 | signal.connect([&](std::string str, int num) { 23 | std::cout << "[" << id << "] on signal: " << str << " " << num << std::endl; 24 | if (str == "done") { 25 | is_running = false; 26 | } 27 | }); 28 | while (is_running) { 29 | std::this_thread::sleep_for(std::chrono::milliseconds(100)); 30 | } 31 | } 32 | 33 | int main() 34 | { 35 | Signal signal; 36 | 37 | std::thread consumer_1([&]() { consumer(1, signal); }); 38 | std::thread consumer_2([&]() { consumer(2, signal); }); 39 | 40 | std::thread producer([&]() { 41 | std::this_thread::sleep_for(std::chrono::milliseconds(100)); 42 | for (int i = 1; i < 6; i++) { 43 | signal.emit("hello", i); 44 | std::this_thread::sleep_for(std::chrono::seconds(1)); 45 | } 46 | signal.emit("done", 114514); 47 | }); 48 | 49 | producer.join(); 50 | consumer_1.join(); 51 | consumer_2.join(); 52 | return 0; 53 | } -------------------------------------------------------------------------------- /example/smooth_lvgl/lvgl_cpp.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file lvgl_cpp.cpp 3 | * @author Forairaaaaa 4 | * @brief 5 | * @version 0.1 6 | * @date 2025-04-26 7 | * 8 | * @copyright Copyright (c) 2025 9 | * 10 | */ 11 | #include "../utils/lvgl_wrapper.h" 12 | #include 13 | #include 14 | #include 15 | 16 | using namespace smooth_ui_toolkit; 17 | using namespace smooth_ui_toolkit::lvgl_cpp; 18 | 19 | int main() 20 | { 21 | lvgl::create_window(800, 520); 22 | 23 | // Basic lvgl object 24 | auto obj = new Container(lv_screen_active()); 25 | obj->setPos(50, 50); 26 | obj->setSize(200, 100); 27 | 28 | // Label 29 | auto label = new Label(lv_screen_active()); 30 | label->setTextFont(&lv_font_montserrat_24); 31 | label->setAlign(LV_ALIGN_CENTER); 32 | label->setText("????????????"); 33 | 34 | // Button 35 | int count = 0; 36 | auto btn = new Button(lv_screen_active()); 37 | btn->setPos(50, 200); 38 | btn->label().setText("+1"); 39 | btn->onClick().connect([&]() { label->setText(fmt::format("{}", count++)); }); 40 | 41 | // Switch 42 | auto sw = new Switch(lv_screen_active()); 43 | sw->setPos(50, 300); 44 | sw->onValueChanged().connect([&](bool value) { label->setText(value ? "ON" : "OFF"); }); 45 | 46 | // Slider 47 | auto slider = new Slider(lv_screen_active()); 48 | slider->setPos(50, 400); 49 | slider->onValueChanged().connect([&](int value) { label->setText(fmt::format("{}", value)); }); 50 | 51 | while (1) { 52 | lvgl::update_window(); 53 | } 54 | 55 | return 0; 56 | } 57 | -------------------------------------------------------------------------------- /example/smooth_lvgl/number_flow.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file number_flow.cpp 3 | * @author Forairaaaaa 4 | * @brief 5 | * @version 0.1 6 | * @date 2025-03-22 7 | * 8 | * @copyright Copyright (c) 2025 9 | * 10 | */ 11 | // Refs: https://number-flow.barvian.me 12 | #include "../utils/lvgl_wrapper.h" 13 | #include 14 | #include 15 | #include 16 | #include 17 | 18 | using namespace smooth_ui_toolkit; 19 | using namespace smooth_ui_toolkit::lvgl_cpp; 20 | 21 | int main() 22 | { 23 | lvgl::create_window(800, 520); 24 | 25 | auto number_flow = new NumberFlow(lv_screen_active()); 26 | number_flow->setAlign(LV_ALIGN_CENTER); 27 | number_flow->setPos(0, -110); 28 | number_flow->setTextFont(&lv_font_montserrat_48); 29 | // number_flow->animationType = animation_type::easing; 30 | // number_flow->transparentBg = false; 31 | // number_flow->showPositiveSign = true; 32 | 33 | auto flex_layout = new Container(lv_screen_active()); 34 | flex_layout->setBorderWidth(0); 35 | flex_layout->setBgOpa(0); 36 | flex_layout->setFlexFlow(LV_FLEX_FLOW_ROW); 37 | flex_layout->setFlexAlign(LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_START); 38 | flex_layout->setSize(LV_SIZE_CONTENT, LV_SIZE_CONTENT); 39 | flex_layout->setAlign(LV_ALIGN_CENTER); 40 | flex_layout->setPos(0, 150); 41 | flex_layout->setPadColumn(50); 42 | 43 | auto btn_next = new Button(flex_layout->get()); 44 | btn_next->label().setText("-1"); 45 | btn_next->onClick().connect([&]() { number_flow->setValue(number_flow->value() - 1); }); 46 | 47 | auto btn_last = new Button(flex_layout->get()); 48 | btn_last->label().setText("+1"); 49 | btn_last->onClick().connect([&]() { number_flow->setValue(number_flow->value() + 1); }); 50 | 51 | auto btn_random = new Button(flex_layout->get()); 52 | btn_random->label().setText("random"); 53 | btn_random->onClick().connect([&]() { 54 | std::random_device rd; 55 | std::mt19937 gen(rd()); 56 | std::uniform_int_distribution dist(-2147483648, 2147483647); 57 | int randomNum = dist(gen); 58 | number_flow->setValue(randomNum); 59 | }); 60 | 61 | int target_value = 0; 62 | auto slider = new Slider(flex_layout->get()); 63 | slider->setRange(1, 9); 64 | slider->setValue(1); 65 | slider->onValueChanged().connect([&](int value) { 66 | int target_value = 0; 67 | for (int i = 1; i <= value; ++i) { 68 | target_value = target_value * 10 + i; 69 | } 70 | number_flow->setValue(target_value); 71 | }); 72 | 73 | while (1) { 74 | number_flow->update(); 75 | lvgl::update_window(); 76 | } 77 | 78 | return 0; 79 | } 80 | -------------------------------------------------------------------------------- /example/utils/lvgl_wrapper.h: -------------------------------------------------------------------------------- 1 | /** 2 | * @file lvgl_wrapper.h 3 | * @author Forairaaaaa 4 | * @brief 5 | * @version 0.1 6 | * @date 2025-01-10 7 | * 8 | * @copyright Copyright (c) 2025 9 | * 10 | */ 11 | #pragma once 12 | #include 13 | #include 14 | 15 | namespace lvgl { 16 | 17 | inline void create_window(int width, int height) 18 | { 19 | lv_init(); 20 | 21 | lv_group_set_default(lv_group_create()); 22 | 23 | auto display = lv_sdl_window_create(width, height); 24 | lv_display_set_default(display); 25 | 26 | auto mouse = lv_sdl_mouse_create(); 27 | lv_indev_set_group(mouse, lv_group_get_default()); 28 | lv_indev_set_display(mouse, display); 29 | 30 | auto mouse_wheel = lv_sdl_mousewheel_create(); 31 | lv_indev_set_display(mouse_wheel, display); 32 | lv_indev_set_group(mouse_wheel, lv_group_get_default()); 33 | 34 | auto keyboard = lv_sdl_keyboard_create(); 35 | lv_indev_set_display(keyboard, display); 36 | lv_indev_set_group(keyboard, lv_group_get_default()); 37 | 38 | lv_obj_set_scroll_dir(lv_screen_active(), LV_DIR_NONE); 39 | } 40 | 41 | inline void update_window() 42 | { 43 | lv_timer_handler(); 44 | } 45 | 46 | class Box { 47 | public: 48 | Box(int width, int height) 49 | { 50 | _box = lv_obj_create(lv_screen_active()); 51 | lv_obj_center(_box); 52 | lv_obj_set_size(_box, width, height); 53 | lv_obj_set_style_transform_pivot_x(_box, width / 2, LV_PART_MAIN); 54 | lv_obj_set_style_transform_pivot_y(_box, height / 2, LV_PART_MAIN); 55 | lv_obj_set_style_radius(_box, (width + height) / 12, LV_PART_MAIN); 56 | lv_obj_set_style_border_color(_box, lv_theme_get_color_primary(lv_screen_active()), LV_PART_MAIN); 57 | } 58 | 59 | void move(const int& x, const int& y, const int& rotate) 60 | { 61 | lv_obj_set_x(_box, x); 62 | lv_obj_set_y(_box, y); 63 | lv_obj_set_style_transform_rotation(_box, rotate * 10, LV_PART_MAIN); 64 | } 65 | 66 | private: 67 | lv_obj_t* _box; 68 | }; 69 | 70 | class Slider { 71 | public: 72 | Slider(const char* nameTag = "", int min = 0, int max = 100) 73 | { 74 | _slider = lv_slider_create(lv_screen_active()); 75 | lv_obj_center(_slider); 76 | range(min, max); 77 | _name_label = lv_label_create(lv_screen_active()); 78 | this->nameTag(nameTag); 79 | _value_label = lv_label_create(lv_screen_active()); 80 | lv_label_set_text_fmt(_value_label, "%d", lv_slider_get_value(_slider)); 81 | } 82 | 83 | void nameTag(const char* nameTag) 84 | { 85 | lv_label_set_text(_name_label, nameTag); 86 | } 87 | 88 | void range(int min, int max) 89 | { 90 | lv_slider_set_range(_slider, min, max); 91 | lv_slider_set_value(_slider, (max + min) / 2, LV_ANIM_ON); 92 | } 93 | 94 | void move(int x, int y) 95 | { 96 | lv_obj_set_x(_slider, x); 97 | lv_obj_set_y(_slider, y); 98 | lv_obj_align_to(_name_label, _slider, LV_ALIGN_LEFT_MID, -80, 0); 99 | lv_obj_align_to(_value_label, _slider, LV_ALIGN_RIGHT_MID, 50, 0); 100 | } 101 | 102 | void onValueChanged(std::function callback) 103 | { 104 | _on_value_changed = callback; 105 | lv_obj_add_event_cb( 106 | _slider, 107 | [](lv_event_t* e) { 108 | lv_obj_t* slider = (lv_obj_t*)lv_event_get_target(e); 109 | auto value = (int)lv_slider_get_value(slider); 110 | auto this_class = (Slider*)lv_event_get_user_data(e); 111 | if (this_class->_on_value_changed) { 112 | this_class->_on_value_changed(value); 113 | } 114 | lv_label_set_text_fmt(this_class->_value_label, "%d", value); 115 | }, 116 | LV_EVENT_VALUE_CHANGED, this); 117 | } 118 | 119 | int value() 120 | { 121 | return lv_slider_get_value(_slider); 122 | } 123 | 124 | void setValue(int value) 125 | { 126 | lv_slider_set_value(_slider, value, LV_ANIM_ON); 127 | lv_label_set_text_fmt(_value_label, "%d", value); 128 | } 129 | 130 | private: 131 | std::function _on_value_changed; 132 | lv_obj_t* _slider; 133 | lv_obj_t* _name_label; 134 | lv_obj_t* _value_label; 135 | }; 136 | 137 | class Button { 138 | public: 139 | Button(const char* label = "") 140 | { 141 | _button = lv_button_create(lv_screen_active()); 142 | lv_obj_center(_button); 143 | _label = lv_label_create(_button); 144 | lv_obj_center(_label); 145 | lv_label_set_text(_label, label); 146 | } 147 | 148 | void move(int x, int y) 149 | { 150 | lv_obj_set_x(_button, x); 151 | lv_obj_set_y(_button, y); 152 | } 153 | 154 | void label(const char* label) 155 | { 156 | lv_label_set_text(_label, label); 157 | } 158 | 159 | void onClick(std::function callback) 160 | { 161 | _on_click = callback; 162 | lv_obj_add_event_cb( 163 | _button, 164 | [](lv_event_t* e) { 165 | lv_obj_t* button = (lv_obj_t*)lv_event_get_target(e); 166 | auto this_class = (Button*)lv_event_get_user_data(e); 167 | if (this_class->_on_click) { 168 | this_class->_on_click(); 169 | } 170 | }, 171 | LV_EVENT_CLICKED, this); 172 | } 173 | 174 | private: 175 | std::function _on_click; 176 | lv_obj_t* _button; 177 | lv_obj_t* _label; 178 | }; 179 | 180 | } // namespace lvgl 181 | -------------------------------------------------------------------------------- /example/utils/raylib_wrapper.h: -------------------------------------------------------------------------------- 1 | /** 2 | * @file raylib_wrapper.h 3 | * @author Forairaaaaa 4 | * @brief 5 | * @version 0.1 6 | * @date 2025-01-06 7 | * 8 | * @copyright Copyright (c) 2025 9 | * 10 | */ 11 | 12 | #pragma once 13 | #include 14 | #include 15 | 16 | namespace raylib { 17 | 18 | inline void create_window(int width, 19 | int height, 20 | const char* title, 21 | std::function onDraw, 22 | std::function onSetup = nullptr) 23 | { 24 | InitWindow(width, height, title); 25 | 26 | if (onSetup) { 27 | onSetup(); 28 | } 29 | 30 | while (!WindowShouldClose()) { 31 | BeginDrawing(); 32 | if (onDraw) { 33 | onDraw(); 34 | } 35 | EndDrawing(); 36 | } 37 | 38 | CloseWindow(); 39 | } 40 | 41 | } // namespace raylib 42 | 43 | inline Color GetRandomColor() 44 | { 45 | Color ret; 46 | ret.r = GetRandomValue(0, 255); 47 | ret.g = GetRandomValue(0, 255); 48 | ret.b = GetRandomValue(0, 255); 49 | ret.a = GetRandomValue(10, 255); 50 | return ret; 51 | } 52 | -------------------------------------------------------------------------------- /library.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "SmoothUIToolKit", 3 | "version": "2.0.0", 4 | "description": "带动画曲线插值的 UI 抽象工具集", 5 | "keywords": "ui, smooth, easing path", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/Forairaaaaa/smooth_ui_toolkit.git" 9 | }, 10 | "authors": [ 11 | { 12 | "name": "Forairaaaaa", 13 | "email": "applesyqd@outlook.com", 14 | "url": "https://github.com/Forairaaaaa" 15 | } 16 | ], 17 | "license": "MIT", 18 | "homepage": "https://github.com/Forairaaaaa/smooth_ui_toolkit.git", 19 | "dependencies": { 20 | }, 21 | "frameworks": "*", 22 | "platforms": "*", 23 | "build" : { 24 | "srcDir": ".", 25 | "includeDir": "./src" 26 | } 27 | } 28 | 29 | -------------------------------------------------------------------------------- /library.properties: -------------------------------------------------------------------------------- 1 | name=SmoothUIToolKit 2 | version=2.0.0 3 | author=Forairaaaaa 4 | maintainer=Forairaaaaa 5 | sentence=带动画曲线插值的 UI 抽象工具集 6 | paragraph=带动画曲线插值的 UI 抽象工具集,无依赖 7 | category=Display 8 | url=https://github.com/Forairaaaaa/smooth_ui_toolkit.git 9 | architectures=* 10 | includes=* 11 | -------------------------------------------------------------------------------- /src/animation/animate/animate.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file animate.cpp 3 | * @author Forairaaaaa 4 | * @brief 5 | * @version 0.1 6 | * @date 2025-01-07 7 | * 8 | * @copyright Copyright (c) 2025 9 | * 10 | */ 11 | #include "animate.h" 12 | #include "utils/hal/hal.h" 13 | #include 14 | 15 | using namespace smooth_ui_toolkit; 16 | 17 | EasingOptions_t& Animate::easingOptions() 18 | { 19 | animationType = animation_type::easing; 20 | return static_cast(get_key_frame_generator()).easingOptions; 21 | } 22 | 23 | SpringOptions_t& Animate::springOptions() 24 | { 25 | animationType = animation_type::spring; 26 | return static_cast(get_key_frame_generator()).springOptions; 27 | } 28 | 29 | void Animate::init() 30 | { 31 | // Setup key frame generator 32 | get_key_frame_generator().start = start; 33 | get_key_frame_generator().end = end; 34 | get_key_frame_generator().init(); 35 | } 36 | 37 | void Animate::play() 38 | { 39 | if (_playing_state == animate_playing_state::playing) { 40 | return; 41 | } 42 | 43 | // If paused, add pause time to start time, resume animation 44 | if (_playing_state == animate_playing_state::paused) { 45 | _start_time += ui_hal::get_tick_s() - _pause_time; 46 | } 47 | // If not, reset repeat count and start time, start animation 48 | else { 49 | _repeat_count = repeat; 50 | _start_time = ui_hal::get_tick_s(); 51 | } 52 | 53 | _playing_state = animate_playing_state::playing; 54 | get_key_frame_generator().done = false; 55 | } 56 | 57 | void Animate::pause() 58 | { 59 | if (_playing_state == animate_playing_state::playing) { 60 | _playing_state = animate_playing_state::paused; 61 | _orchestration_state = animate_orchestration_state::on_delay; 62 | _pause_time = ui_hal::get_tick_s(); 63 | } 64 | } 65 | 66 | void Animate::complete() 67 | { 68 | if (_playing_state == animate_playing_state::completed) { 69 | return; 70 | } 71 | _playing_state = animate_playing_state::completed; 72 | _orchestration_state = animate_orchestration_state::on_delay; 73 | get_key_frame_generator().done = false; 74 | get_key_frame_generator().value = end; 75 | } 76 | 77 | void Animate::cancel() 78 | { 79 | if (_playing_state == animate_playing_state::cancelled) { 80 | return; 81 | } 82 | _playing_state = animate_playing_state::cancelled; 83 | _orchestration_state = animate_orchestration_state::on_delay; 84 | get_key_frame_generator().done = false; 85 | get_key_frame_generator().value = start; 86 | } 87 | 88 | void Animate::retarget(const float& start, const float& end) 89 | { 90 | get_key_frame_generator().retarget(start, end); 91 | if (_playing_state != animate_playing_state::paused) { 92 | _playing_state = animate_playing_state::idle; 93 | play(); 94 | } 95 | } 96 | 97 | void Animate::update() 98 | { 99 | update_orchestration_state_fsm(); 100 | } 101 | 102 | void Animate::update_playing_state_fsm() 103 | { 104 | if (done()) { 105 | return; 106 | } 107 | if (_playing_state == animate_playing_state::idle || _playing_state == animate_playing_state::paused) { 108 | return; 109 | } 110 | 111 | // If called complete or cancel method 112 | if (_playing_state == animate_playing_state::completed || _playing_state == animate_playing_state::cancelled) { 113 | get_key_frame_generator().done = true; 114 | if (_on_update) { 115 | _on_update(value()); 116 | } 117 | return; 118 | } 119 | 120 | // Update key frame 121 | get_key_frame_generator().next(ui_hal::get_tick_s() - _start_time); 122 | if (_on_update) { 123 | _on_update(value()); 124 | } 125 | 126 | // Update playing state 127 | if (_playing_state == animate_playing_state::playing && done()) { 128 | _playing_state = animate_playing_state::completed; 129 | if (_on_complete) { 130 | _on_complete(); 131 | } 132 | } 133 | } 134 | 135 | void Animate::update_orchestration_state_fsm() 136 | { 137 | // Handle on delay 138 | if (_orchestration_state == animate_orchestration_state::on_delay) { 139 | if (_playing_state != animate_playing_state::idle && _playing_state != animate_playing_state::paused) { 140 | // Invoke callback 141 | if (_on_update) { 142 | _on_update(value()); 143 | } 144 | // Check delay timeout 145 | if (ui_hal::get_tick_s() - _start_time >= delay) { 146 | _orchestration_state = animate_orchestration_state::on_playing; 147 | _start_time = ui_hal::get_tick_s(); 148 | } 149 | } 150 | } 151 | 152 | // Handle on playing 153 | else if (_orchestration_state == animate_orchestration_state::on_playing) { 154 | update_playing_state_fsm(); 155 | if (done() && _repeat_count != 0) { 156 | // Decrement repeat count 157 | if (_repeat_count > 0) { 158 | _repeat_count--; 159 | } 160 | _orchestration_state = animate_orchestration_state::on_repeat_delay; 161 | _start_time = ui_hal::get_tick_s(); 162 | } 163 | } 164 | 165 | // Handle on repeat delay 166 | else { 167 | // Check repeat delay timeout 168 | if (ui_hal::get_tick_s() - _start_time >= repeatDelay) { 169 | // Reset animation 170 | if (repeatType == animate_repeat_type::reverse) { 171 | std::swap(start, end); 172 | } 173 | init(); 174 | _playing_state = animate_playing_state::playing; 175 | _orchestration_state = animate_orchestration_state::on_delay; 176 | _start_time = ui_hal::get_tick_s(); 177 | } 178 | } 179 | } 180 | 181 | // Lazy loading, default spring 182 | KeyFrameGenerator& Animate::get_key_frame_generator() 183 | { 184 | if (_key_frame_generator) { 185 | if (_key_frame_generator->type() != animationType) { 186 | _key_frame_generator.reset(); 187 | } 188 | } 189 | if (!_key_frame_generator) { 190 | if (animationType == animation_type::spring) { 191 | _key_frame_generator = std::make_shared(); 192 | } else if (animationType == animation_type::easing) { 193 | _key_frame_generator = std::make_shared(); 194 | } 195 | } 196 | return *_key_frame_generator; 197 | } 198 | -------------------------------------------------------------------------------- /src/animation/animate/animate.h: -------------------------------------------------------------------------------- 1 | /** 2 | * @file animate.h 3 | * @author Forairaaaaa 4 | * @brief 5 | * @version 0.1 6 | * @date 2025-01-07 7 | * 8 | * @copyright Copyright (c) 2025 9 | * 10 | */ 11 | #pragma once 12 | #include "../generators/generators.h" 13 | #include "../generators/spring/spring.h" 14 | #include "../generators/easing/easing.h" 15 | #include 16 | #include 17 | // 参数参考:https://motion.dev/docs/animate#options 18 | 19 | namespace smooth_ui_toolkit { 20 | 21 | namespace animate_repeat_type { 22 | enum Type_t { 23 | loop = 0, // Repeats the animation from the start 24 | reverse, // Alternates between forward and backwards playback 25 | }; 26 | } 27 | 28 | namespace animate_playing_state { 29 | enum State_t { 30 | idle = 0, 31 | playing, 32 | paused, 33 | completed, 34 | cancelled, 35 | }; 36 | } 37 | 38 | namespace animate_orchestration_state { 39 | enum State_t { 40 | on_delay = 0, 41 | on_playing, 42 | on_repeat_delay, 43 | }; 44 | } 45 | 46 | class Animate { 47 | public: 48 | Animate() {} 49 | virtual ~Animate() {} 50 | 51 | // Start value 52 | float start = 0.0f; 53 | // End value 54 | float end = 0.0f; 55 | // Delay the animation by this duration (in seconds) 56 | float delay = 0.0f; 57 | // The number of times to repeat the transition. Set to -1 for perpetual animation 58 | int repeat = 0; 59 | // How to repeat the animation 60 | animate_repeat_type::Type_t repeatType = animate_repeat_type::loop; 61 | // When repeating an animation, repeatDelay will set the duration of the time to wait, in seconds, between each 62 | // repetition 63 | float repeatDelay = 0.0f; 64 | // Animation type 65 | animation_type::Type_t animationType = animation_type::spring; 66 | // Easing animation options, call this method will set animation type to easing 67 | EasingOptions_t& easingOptions(); 68 | // Spring animation options, call this method will set animation type to spring 69 | SpringOptions_t& springOptions(); 70 | // On value update 71 | void onUpdate(std::function callback) 72 | { 73 | _on_update = callback; 74 | } 75 | // On animation complete 76 | void onComplete(std::function callback) 77 | { 78 | _on_complete = callback; 79 | } 80 | 81 | /** 82 | * @brief Init animation 83 | * 84 | */ 85 | void init(); 86 | 87 | /** 88 | * @brief Start playing animation, If an animation is paused, it will resume from its current time, If animation has 89 | * finished, it will restart 90 | * 91 | */ 92 | void play(); 93 | 94 | /** 95 | * @brief Pauses the animation until resumed with play() 96 | * 97 | */ 98 | void pause(); 99 | 100 | /** 101 | * @brief Immediately completes the animation, running it to the end state 102 | * 103 | */ 104 | void complete(); 105 | 106 | /** 107 | * @brief Cancels the animation, reverting it to the initial state 108 | * 109 | */ 110 | void cancel(); 111 | 112 | /** 113 | * @brief Reset start and end value dynamically, spring animation will animate to new value with current velocity 114 | * 115 | * @param start 116 | * @param end 117 | */ 118 | void retarget(const float& start, const float& end); 119 | 120 | /** 121 | * @brief Update animation, keep calling this method to update animation, callbacks will be invoked in this method 122 | * 123 | */ 124 | void update(); 125 | 126 | /** 127 | * @brief Is key frame generator done 128 | * 129 | * @return true 130 | * @return false 131 | */ 132 | inline bool done() 133 | { 134 | return get_key_frame_generator().done; 135 | } 136 | 137 | /** 138 | * @brief Get key frame generator current value 139 | * 140 | * @return float 141 | */ 142 | inline float value() 143 | { 144 | return get_key_frame_generator().value; 145 | } 146 | 147 | inline animate_playing_state::State_t currentPlayingState() 148 | { 149 | return _playing_state; 150 | } 151 | 152 | protected: 153 | std::function _on_update; 154 | std::function _on_complete; 155 | std::shared_ptr _key_frame_generator; 156 | KeyFrameGenerator& get_key_frame_generator(); 157 | animate_playing_state::State_t _playing_state = animate_playing_state::idle; 158 | animate_orchestration_state::State_t _orchestration_state = animate_orchestration_state::on_delay; 159 | float _start_time = 0.0f; 160 | float _pause_time = 0.0f; 161 | int _repeat_count = 0; 162 | 163 | void update_playing_state_fsm(); 164 | void update_orchestration_state_fsm(); 165 | }; 166 | 167 | } // namespace smooth_ui_toolkit 168 | -------------------------------------------------------------------------------- /src/animation/animate_value/animate_value.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file animate_value.cpp 3 | * @author Forairaaaaa 4 | * @brief 5 | * @version 0.1 6 | * @date 2025-01-08 7 | * 8 | * @copyright Copyright (c) 2025 9 | * 10 | */ 11 | #include "animate_value.h" 12 | 13 | using namespace smooth_ui_toolkit; 14 | 15 | AnimateValue::AnimateValue() 16 | { 17 | begin(); 18 | } 19 | 20 | AnimateValue::AnimateValue(float defaultValue) : _default_value(defaultValue) 21 | { 22 | begin(); 23 | } 24 | 25 | AnimateValue& AnimateValue::operator=(float newValue) 26 | { 27 | move(newValue); 28 | return *this; 29 | } 30 | 31 | AnimateValue::operator float() 32 | { 33 | return value(); 34 | } 35 | 36 | void AnimateValue::begin() 37 | { 38 | if (_is_begin) { 39 | return; 40 | } 41 | _is_begin = true; 42 | start = _default_value; 43 | end = _default_value; 44 | get_key_frame_generator().value = _default_value; 45 | init(); 46 | play(); 47 | } 48 | 49 | void AnimateValue::stop() 50 | { 51 | if (!_is_begin) { 52 | return; 53 | } 54 | _is_begin = false; 55 | _default_value = value(); 56 | } 57 | 58 | void AnimateValue::teleport(float newValue) 59 | { 60 | bool is_begin = _is_begin; 61 | stop(); 62 | _default_value = newValue; 63 | if (is_begin) { 64 | begin(); 65 | } 66 | } 67 | 68 | void AnimateValue::move(float newValue) 69 | { 70 | // If not begin yet, set to default value 71 | if (!_is_begin) { 72 | _default_value = newValue; 73 | return; 74 | } 75 | // If same value, do nothing 76 | if (newValue == get_key_frame_generator().end) { 77 | return; 78 | } 79 | // Else, retarget to new value 80 | retarget(value(), newValue); 81 | return; 82 | } 83 | 84 | float AnimateValue::value() 85 | { 86 | if (!_is_begin) { 87 | return _default_value; 88 | } 89 | update(); 90 | return get_key_frame_generator().value; 91 | } 92 | 93 | float AnimateValue::directValue() 94 | { 95 | if (!_is_begin) { 96 | return _default_value; 97 | } 98 | return get_key_frame_generator().value; 99 | } 100 | -------------------------------------------------------------------------------- /src/animation/animate_value/animate_value.h: -------------------------------------------------------------------------------- 1 | /** 2 | * @file animate_value.h 3 | * @author Forairaaaaa 4 | * @brief 5 | * @version 0.1 6 | * @date 2025-01-08 7 | * 8 | * @copyright Copyright (c) 2025 9 | * 10 | */ 11 | #pragma once 12 | #include "../animate/animate.h" 13 | 14 | namespace smooth_ui_toolkit { 15 | 16 | class AnimateValue : public Animate { 17 | public: 18 | AnimateValue(); 19 | AnimateValue(float defaultValue); 20 | ~AnimateValue() {} 21 | 22 | // Override assignment operator 23 | AnimateValue& operator=(float newValue); 24 | 25 | // Override type conversion 26 | operator float(); 27 | 28 | /** 29 | * @brief Begin value animation 30 | * 31 | */ 32 | void begin(); 33 | 34 | /** 35 | * @brief Stop value animation 36 | * 37 | */ 38 | void stop(); 39 | 40 | /** 41 | * @brief Set to a new value immediately 42 | * 43 | */ 44 | void teleport(float newValue); 45 | 46 | /** 47 | * @brief Move to a new value 48 | * 49 | * @param newValue 50 | */ 51 | void move(float newValue); 52 | 53 | /** 54 | * @brief Current value 55 | * 56 | * @return float 57 | */ 58 | float value(); 59 | 60 | /** 61 | * @brief Get current value without trigger auto update 62 | * 63 | * @return float 64 | */ 65 | float directValue(); 66 | 67 | private: 68 | bool _is_begin = false; 69 | float _default_value = 0.0f; 70 | }; 71 | 72 | } // namespace smooth_ui_toolkit 73 | -------------------------------------------------------------------------------- /src/animation/generators/easing/easing.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file easing.cpp 3 | * @author Forairaaaaa 4 | * @brief 5 | * @version 0.1 6 | * @date 2025-03-29 7 | * 8 | * @copyright Copyright (c) 2025 9 | * 10 | */ 11 | #include "easing.h" 12 | 13 | using namespace smooth_ui_toolkit; 14 | 15 | void Easing::init() 16 | { 17 | done = false; 18 | value = start; 19 | } 20 | 21 | void Easing::retarget(const float& start, const float& end) 22 | { 23 | this->start = start; 24 | this->end = end; 25 | done = false; 26 | } 27 | 28 | bool Easing::next(const float& t) 29 | { 30 | if (done) { 31 | return done; 32 | } 33 | 34 | float progress = t / easingOptions.duration; 35 | if (progress >= 1.0f) { 36 | value = end; 37 | done = true; 38 | } else { 39 | value = start + (end - start) * easingOptions.easingFunction(progress); 40 | } 41 | 42 | return done; 43 | } 44 | -------------------------------------------------------------------------------- /src/animation/generators/easing/easing.h: -------------------------------------------------------------------------------- 1 | /** 2 | * @file easing.h 3 | * @author Forairaaaaa 4 | * @brief 5 | * @version 0.1 6 | * @date 2025-03-29 7 | * 8 | * @copyright Copyright (c) 2025 9 | * 10 | */ 11 | #pragma once 12 | #include "../generators.h" 13 | #include "../../../utils/easing/ease.h" 14 | #include 15 | 16 | namespace smooth_ui_toolkit { 17 | 18 | struct EasingOptions_t { 19 | float duration = 1.0f; // 动画持续时间,单位 s 20 | std::function easingFunction = ease::ease_in_out_quad; // 缓动函数 21 | }; 22 | 23 | class Easing : public KeyFrameGenerator { 24 | public: 25 | Easing() {} 26 | ~Easing() {} 27 | 28 | EasingOptions_t easingOptions; 29 | 30 | inline void setEasingOptions(float duration, std::function easingFunction) 31 | { 32 | easingOptions.duration = duration; 33 | easingOptions.easingFunction = easingFunction; 34 | } 35 | inline void setEasingOptions(const EasingOptions_t& options) 36 | { 37 | easingOptions = options; 38 | } 39 | 40 | virtual void init() override; 41 | virtual void retarget(const float& start, const float& end) override; 42 | virtual bool next(const float& t) override; 43 | virtual animation_type::Type_t type() const override 44 | { 45 | return animation_type::easing; 46 | } 47 | }; 48 | 49 | } // namespace smooth_ui_toolkit 50 | -------------------------------------------------------------------------------- /src/animation/generators/generators.h: -------------------------------------------------------------------------------- 1 | /** 2 | * @file generators.h 3 | * @author Forairaaaaa 4 | * @brief 5 | * @version 0.1 6 | * @date 2025-01-06 7 | * 8 | * @copyright Copyright (c) 2025 9 | * 10 | */ 11 | #pragma once 12 | 13 | namespace smooth_ui_toolkit { 14 | 15 | namespace animation_type { 16 | enum Type_t { 17 | spring = 0, 18 | easing, 19 | }; 20 | } // namespace animation_type 21 | 22 | class KeyFrameGenerator { 23 | public: 24 | KeyFrameGenerator() {} 25 | virtual ~KeyFrameGenerator() {} 26 | 27 | float start = 0.0f; 28 | float end = 0.0f; 29 | float value = 0.0f; 30 | bool done = false; 31 | 32 | virtual void init() {} 33 | 34 | virtual void retarget(const float& start, const float& end) {} 35 | 36 | virtual bool next(const float& t) 37 | { 38 | return done; 39 | } 40 | 41 | virtual animation_type::Type_t type() const = 0; 42 | }; 43 | 44 | } // namespace smooth_ui_toolkit 45 | -------------------------------------------------------------------------------- /src/animation/generators/spring/spring.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file spring.cpp 3 | * @author Forairaaaaa 4 | * @brief 5 | * @version 0.1 6 | * @date 2025-01-06 7 | * 8 | * @copyright Copyright (c) 2025 9 | * 10 | */ 11 | #include "spring.h" 12 | #include 13 | #include 14 | 15 | using namespace smooth_ui_toolkit; 16 | 17 | void Spring::setSpringOptions(float duration, float bounce, float visualDuration) 18 | { 19 | // 默认质量 20 | springOptions.mass = 1.0f; 21 | 22 | // Clamp 函数,确保 bounce 在合理范围内 23 | auto clamp = [](float value, float min, float max) { return std::max(min, std::min(value, max)); }; 24 | 25 | // 确保 bounce 在 0.05 到 1 之间 26 | bounce = clamp(bounce, 0.05f, 1.0f); 27 | 28 | // 根据 visualDuration 计算 stiffness 和 damping 29 | if (visualDuration > 0) { 30 | float root = (2 * M_PI) / (visualDuration * 1.2f); // 可视化周期 31 | springOptions.stiffness = root * root; // 刚度 32 | springOptions.damping = 33 | 2 * (1.0f - bounce) * std::sqrt(springOptions.stiffness * springOptions.mass); // 阻尼系数 34 | } else { 35 | // 如果未提供 visualDuration,使用默认的 duration 和 bounce 来推导 36 | springOptions.stiffness = (36 / (duration / 1000.0f) / (duration / 1000.0f)); // 刚度 37 | springOptions.damping = 38 | 2 * (1.0f - bounce) * std::sqrt(springOptions.stiffness * springOptions.mass); // 阻尼系数 39 | } 40 | } 41 | 42 | void Spring::init() 43 | { 44 | done = false; 45 | value = start; 46 | 47 | // If or duration or visualDuration is provided, set spring options by duration/bounce-based options 48 | if (springOptions.duration > 0 || springOptions.visualDuration > 0) { 49 | setSpringOptions(springOptions.duration, springOptions.bounce, springOptions.visualDuration); 50 | } 51 | 52 | _undamped_angular_freq = std::sqrt(springOptions.stiffness / springOptions.mass); 53 | _damping_ratio = springOptions.damping / (2 * std::sqrt(springOptions.stiffness * springOptions.mass)); 54 | 55 | float initialDelta = end - start; 56 | 57 | // 根据阻尼比选择不同的公式 58 | if (_damping_ratio < 1) { 59 | float angularFreq = calc_angular_freq(_undamped_angular_freq, _damping_ratio); 60 | 61 | // 欠阻尼 62 | // Underdamped spring 63 | _resolve_spring = [=](float t) { 64 | float envelope = std::exp(-_damping_ratio * _undamped_angular_freq * t); 65 | 66 | return (end - 67 | envelope * (((springOptions.velocity + _damping_ratio * _undamped_angular_freq * initialDelta) / 68 | angularFreq) * 69 | std::sin(angularFreq * t) + 70 | initialDelta * std::cos(angularFreq * t))); 71 | }; 72 | } else if (_damping_ratio == 1) { 73 | // 临界阻尼 74 | // Critically damped spring 75 | _resolve_spring = [=](float t) { 76 | return end - std::exp(-_undamped_angular_freq * t) * 77 | (initialDelta + (springOptions.velocity + _undamped_angular_freq * initialDelta) * t); 78 | }; 79 | } else { 80 | // 过阻尼 81 | // Overdamped spring 82 | float dampedAngularFreq = _undamped_angular_freq * std::sqrt(_damping_ratio * _damping_ratio - 1); 83 | _resolve_spring = [=](float t) { 84 | float envelope = std::exp(-_damping_ratio * _undamped_angular_freq * t); 85 | 86 | // When performing sinh or cosh values can hit Infinity so we cap them here 87 | float freqForT = std::min(dampedAngularFreq * t, 300.0f); 88 | 89 | return (end - 90 | (envelope * ((springOptions.velocity + _damping_ratio * _undamped_angular_freq * initialDelta) * 91 | std::sinh(freqForT) + 92 | dampedAngularFreq * initialDelta * std::cosh(freqForT))) / 93 | dampedAngularFreq); 94 | }; 95 | } 96 | } 97 | 98 | void Spring::retarget(const float& start, const float& end) 99 | { 100 | springOptions.velocity = -_current_velocity; 101 | this->start = start; 102 | this->end = end; 103 | init(); 104 | } 105 | 106 | bool Spring::next(const float& t) 107 | { 108 | if (done) { 109 | return done; 110 | } 111 | 112 | // 根据当前时间计算弹簧的值 113 | if (_resolve_spring) { 114 | value = _resolve_spring(t); 115 | } else { 116 | init(); 117 | } 118 | 119 | // 检查是否接近静止 120 | calc_velocity(t); 121 | bool isBelowVelocityThreshold = std::abs(_current_velocity) <= springOptions.restSpeed; 122 | bool isBelowDisplacementThreshold = std::abs(end - value) <= springOptions.restDelta; 123 | done = isBelowVelocityThreshold && isBelowDisplacementThreshold; 124 | 125 | return done; 126 | } 127 | 128 | void Spring::calc_velocity(const float& t) 129 | { 130 | // 简单近似速度计算 131 | const float dt = 1e-5; // 微小时间增量 132 | _current_velocity = (_resolve_spring(t + dt) - _resolve_spring(t)) / dt; 133 | } 134 | 135 | float Spring::calc_angular_freq(float undampedFreq, float dampingRatio) 136 | { 137 | return undampedFreq * std::sqrt(1 - dampingRatio * dampingRatio); 138 | } 139 | -------------------------------------------------------------------------------- /src/animation/generators/spring/spring.h: -------------------------------------------------------------------------------- 1 | /** 2 | * @file spring.h 3 | * @author Forairaaaaa 4 | * @brief 5 | * @version 0.1 6 | * @date 2025-01-06 7 | * 8 | * @copyright Copyright (c) 2025 9 | * 10 | */ 11 | #pragma once 12 | #include "../generators.h" 13 | #include 14 | // 参考: 15 | // https://github.com/motiondivision/motion/blob/main/packages/framer-motion/src/animation/generators/spring 16 | 17 | namespace smooth_ui_toolkit { 18 | 19 | struct SpringOptions_t { 20 | float stiffness = 100.0; // 弹性系数 21 | float damping = 10.0; // 阻尼系数 22 | float mass = 1.0; // 质量 23 | float velocity = 0.0; // 初始速度 24 | float restSpeed = 0.1; // 静止速度阈值 25 | float restDelta = 0.1; // 静止位置阈值 26 | float duration = 0.0; // 动画持续时间 ms 27 | float bounce = 0.3; // 反弹系数 0.05~1.0 28 | float visualDuration = 0.0; // 可视化时间 29 | }; 30 | 31 | class Spring : public KeyFrameGenerator { 32 | public: 33 | Spring() {} 34 | ~Spring() {} 35 | 36 | SpringOptions_t springOptions; 37 | 38 | /** 39 | * @brief Set spring options by duration/bounce-based options 40 | * 41 | * @param duration in ms 42 | * @param bounce 0.05~1.0 43 | * @param visualDuration in seconds 44 | */ 45 | void setSpringOptions(float duration = 800.0f, float bounce = 0.3f, float visualDuration = 0.3f); 46 | inline void setSpringOptions(const SpringOptions_t& options) 47 | { 48 | springOptions = options; 49 | } 50 | 51 | virtual void init() override; 52 | virtual void retarget(const float& start, const float& end) override; 53 | virtual bool next(const float& t) override; 54 | virtual animation_type::Type_t type() const override 55 | { 56 | return animation_type::spring; 57 | } 58 | 59 | protected: 60 | float _damping_ratio; // 阻尼比 61 | float _undamped_angular_freq; // 未阻尼角频率 62 | float _current_velocity; // 当前速度 63 | std::function _resolve_spring; // 动画计算公式 64 | 65 | void calc_velocity(const float& t); 66 | float calc_angular_freq(float undampedFreq, float dampingRatio); 67 | }; 68 | 69 | } // namespace smooth_ui_toolkit 70 | -------------------------------------------------------------------------------- /src/animation/sequence/animate_sequence.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file animate_sequence.cpp 3 | * @author Forairaaaaa 4 | * @brief 5 | * @version 0.1 6 | * @date 2025-04-04 7 | * 8 | * @copyright Copyright (c) 2025 9 | * 10 | */ 11 | #include "animate_sequence.h" 12 | #include 13 | #include 14 | 15 | using namespace smooth_ui_toolkit; 16 | 17 | AnimateSequence::operator float() 18 | { 19 | return value(); 20 | } 21 | 22 | void AnimateSequence::play() 23 | { 24 | if (_animate_value) { 25 | _animate_value->play(); 26 | return; 27 | } 28 | // Reset index and create animate value 29 | if (!_value_sequence.empty()) { 30 | _current_index = 0; 31 | _current_value = _value_sequence[_current_index]; 32 | _animate_value = std::make_unique(_current_value); 33 | if (_on_play) { 34 | _on_play(*_animate_value); 35 | } 36 | _repeat_count = repeat; 37 | _is_done = false; 38 | } 39 | } 40 | 41 | void AnimateSequence::pause() 42 | { 43 | if (_animate_value) { 44 | _animate_value->pause(); 45 | } 46 | } 47 | 48 | void AnimateSequence::complete() 49 | { 50 | if (!_value_sequence.empty()) { 51 | _current_value = _value_sequence.back(); 52 | } 53 | _animate_value.reset(); 54 | _is_done = true; 55 | if (_on_done) { 56 | _on_done(); 57 | } 58 | } 59 | 60 | void AnimateSequence::cancel() 61 | { 62 | if (!_value_sequence.empty()) { 63 | _current_value = _value_sequence[1]; 64 | } 65 | _animate_value.reset(); 66 | _is_done = true; 67 | if (_on_done) { 68 | _on_done(); 69 | } 70 | } 71 | 72 | void AnimateSequence::update() 73 | { 74 | // If animate value instance exists 75 | if (_animate_value) { 76 | _current_value = _animate_value->value(); 77 | // If animation is done, retarget to next value 78 | if (_animate_value->done()) { 79 | int next_index = _current_index + 1; 80 | if (next_index < _value_sequence.size()) { 81 | _current_index = next_index; 82 | _animate_value->stop(); 83 | if (_on_step) { 84 | _on_step(*_animate_value, _value_sequence, _current_index); 85 | } 86 | _animate_value->begin(); 87 | _animate_value->cancel(); 88 | _animate_value->teleport(_current_value); 89 | _animate_value->move(_value_sequence[_current_index]); 90 | } 91 | // If reached the end of the sequence 92 | else { 93 | // If repeat is not set, stop the animation 94 | if (_repeat_count == 0) { 95 | _animate_value.reset(); 96 | _is_done = true; 97 | if (_on_done) { 98 | _on_done(); 99 | } 100 | return; 101 | } 102 | // Count down repeat 103 | if (_repeat_count > 0) { 104 | _repeat_count--; 105 | } 106 | // Reset animation 107 | if (repeatType == animate_repeat_type::reverse) { 108 | std::reverse(_value_sequence.begin(), _value_sequence.end()); 109 | } 110 | _current_index = 0; 111 | _current_value = _value_sequence[_current_index]; 112 | _animate_value->teleport(_current_value); 113 | } 114 | } 115 | } 116 | } 117 | 118 | float AnimateSequence::value() 119 | { 120 | update(); 121 | return _current_value; 122 | } 123 | -------------------------------------------------------------------------------- /src/animation/sequence/animate_sequence.h: -------------------------------------------------------------------------------- 1 | /** 2 | * @file animate_sequence.h 3 | * @author Forairaaaaa 4 | * @brief 5 | * @version 0.1 6 | * @date 2025-04-04 7 | * 8 | * @copyright Copyright (c) 2025 9 | * 10 | */ 11 | #pragma once 12 | #include "../animate/animate.h" 13 | #include "../animate_value/animate_value.h" 14 | #include 15 | #include 16 | #include 17 | 18 | namespace smooth_ui_toolkit { 19 | 20 | class AnimateSequence { 21 | public: 22 | AnimateSequence() {} 23 | AnimateSequence(const std::initializer_list& valueSequence) 24 | { 25 | setSequence(valueSequence); 26 | } 27 | template 28 | AnimateSequence(const std::vector& valueSequence) 29 | { 30 | setSequence(valueSequence); 31 | } 32 | ~AnimateSequence() {} 33 | 34 | // Override assignment operator 35 | AnimateSequence& operator=(const std::initializer_list& valueSequence) 36 | { 37 | setSequence(valueSequence); 38 | return *this; 39 | } 40 | template 41 | AnimateSequence& operator=(const std::vector& valueSequence) 42 | { 43 | setSequence(valueSequence); 44 | return *this; 45 | } 46 | 47 | // Override type conversion 48 | operator float(); 49 | 50 | // no copy constructor and copy assignment operator 51 | AnimateSequence(const AnimateSequence&) = delete; 52 | AnimateSequence& operator=(const AnimateSequence&) = delete; 53 | 54 | void setSequence(const std::initializer_list& valueSequence) 55 | { 56 | _value_sequence.reserve(valueSequence.size()); 57 | for (const auto& v : valueSequence) { 58 | _value_sequence.push_back(v); 59 | } 60 | } 61 | template 62 | void setSequence(const std::vector& valueSequence) 63 | { 64 | _value_sequence.reserve(valueSequence.size()); 65 | for (const auto& v : valueSequence) { 66 | _value_sequence.push_back(static_cast(v)); 67 | } 68 | } 69 | 70 | int repeat = 0; 71 | animate_repeat_type::Type_t repeatType = animate_repeat_type::loop; 72 | std::vector& sequence() 73 | { 74 | return _value_sequence; 75 | } 76 | void onPlay(std::function onPlay) 77 | { 78 | _on_play = onPlay; 79 | } 80 | void onStep(std::function& sequence, int step)> onStep) 81 | { 82 | _on_step = onStep; 83 | } 84 | void onDone(std::function onDone) 85 | { 86 | _on_done = onDone; 87 | } 88 | 89 | void play(); 90 | void pause(); 91 | void complete(); 92 | void cancel(); 93 | void update(); 94 | float value(); 95 | float directValue() 96 | { 97 | return _current_value; 98 | } 99 | bool done() 100 | { 101 | return _is_done; 102 | } 103 | 104 | private: 105 | float _current_value = 0.0f; 106 | int _current_index = 0; 107 | int _repeat_count = 0; 108 | bool _is_done = true; 109 | 110 | std::vector _value_sequence; 111 | std::unique_ptr _animate_value; 112 | 113 | std::function _on_play; 114 | std::function& sequence, int step)> _on_step; 115 | std::function _on_done; 116 | }; 117 | 118 | } // namespace smooth_ui_toolkit 119 | -------------------------------------------------------------------------------- /src/lvgl/lvgl_cpp/button.h: -------------------------------------------------------------------------------- 1 | /** 2 | * @file button.h 3 | * @author Forairaaaaa 4 | * @brief 5 | * @version 0.1 6 | * @date 2025-01-18 7 | * 8 | * @copyright Copyright (c) 2025 9 | * 10 | */ 11 | #pragma once 12 | #include "obj.h" 13 | #include "label.h" 14 | #include 15 | 16 | namespace smooth_ui_toolkit { 17 | namespace lvgl_cpp { 18 | 19 | /** 20 | * @brief Lvgl button 21 | * 22 | */ 23 | class Button : public Widget { 24 | public: 25 | using Widget::Widget; 26 | 27 | Label& label() 28 | { 29 | if (!_label) { 30 | _label = std::make_unique