├── .github └── workflows │ └── firebase-hosting-pull-request.yml ├── .gitignore ├── .metadata ├── CHANGELOG.md ├── LICENSE ├── README.md ├── analysis_option.yaml ├── example ├── .gitignore ├── .metadata ├── README.md ├── android │ ├── .gitignore │ ├── android.zip │ ├── app │ │ ├── build.gradle │ │ └── src │ │ │ ├── debug │ │ │ └── AndroidManifest.xml │ │ │ ├── main │ │ │ ├── AndroidManifest.xml │ │ │ ├── java │ │ │ │ └── com │ │ │ │ │ └── example │ │ │ │ │ └── example │ │ │ │ │ └── MainActivity.java │ │ │ ├── kotlin │ │ │ │ └── com │ │ │ │ │ └── example │ │ │ │ │ └── example │ │ │ │ │ └── MainActivity.kt │ │ │ └── res │ │ │ │ ├── drawable-v21 │ │ │ │ └── launch_background.xml │ │ │ │ ├── drawable │ │ │ │ └── launch_background.xml │ │ │ │ ├── mipmap-hdpi │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-mdpi │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xhdpi │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ └── ic_launcher.png │ │ │ │ ├── values-night │ │ │ │ └── styles.xml │ │ │ │ └── values │ │ │ │ └── styles.xml │ │ │ └── profile │ │ │ └── AndroidManifest.xml │ ├── build.gradle │ ├── gradle.properties │ ├── gradle │ │ └── wrapper │ │ │ └── gradle-wrapper.properties │ └── settings.gradle ├── assets │ └── images │ │ ├── Icon-192.png │ │ └── Icon-512.png ├── ios │ ├── .gitignore │ ├── Flutter │ │ ├── AppFrameworkInfo.plist │ │ ├── Debug.xcconfig │ │ └── Release.xcconfig │ ├── Runner.xcodeproj │ │ ├── project.pbxproj │ │ ├── project.xcworkspace │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcshareddata │ │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ │ └── WorkspaceSettings.xcsettings │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ └── Runner.xcscheme │ ├── Runner.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ └── WorkspaceSettings.xcsettings │ └── Runner │ │ ├── AppDelegate.swift │ │ ├── Assets.xcassets │ │ ├── AppIcon.appiconset │ │ │ ├── Contents.json │ │ │ ├── Icon-App-1024x1024@1x.png │ │ │ ├── Icon-App-20x20@1x.png │ │ │ ├── Icon-App-20x20@2x.png │ │ │ ├── Icon-App-20x20@3x.png │ │ │ ├── Icon-App-29x29@1x.png │ │ │ ├── Icon-App-29x29@2x.png │ │ │ ├── Icon-App-29x29@3x.png │ │ │ ├── Icon-App-40x40@1x.png │ │ │ ├── Icon-App-40x40@2x.png │ │ │ ├── Icon-App-40x40@3x.png │ │ │ ├── Icon-App-60x60@2x.png │ │ │ ├── Icon-App-60x60@3x.png │ │ │ ├── Icon-App-76x76@1x.png │ │ │ ├── Icon-App-76x76@2x.png │ │ │ └── Icon-App-83.5x83.5@2x.png │ │ └── LaunchImage.imageset │ │ │ ├── Contents.json │ │ │ ├── LaunchImage.png │ │ │ ├── LaunchImage@2x.png │ │ │ ├── LaunchImage@3x.png │ │ │ └── README.md │ │ ├── Base.lproj │ │ ├── LaunchScreen.storyboard │ │ └── Main.storyboard │ │ ├── Info.plist │ │ └── Runner-Bridging-Header.h ├── lib │ ├── color_picker.dart │ ├── edit_shape_page.dart │ ├── gradient_picker.dart │ ├── how_to_use_text.dart │ ├── main.dart │ ├── morph_shape_page.dart │ ├── shape_picker.dart │ └── value_pickers.dart ├── pubspec.yaml └── test │ └── widget_test.dart ├── lib ├── morphable_shape.dart ├── preset_shape_map.dart └── src │ ├── animated_decorated_shadowd_shape.dart │ ├── common_includes.dart │ ├── decorated_shadowed_shape.dart │ ├── dynamic_path │ ├── border_paths.dart │ ├── dynamic_path.dart │ └── dynamic_path_morph.dart │ ├── morphable_shape_border.dart │ ├── morphable_shape_border_tween.dart │ ├── parse_json.dart │ ├── shape_borders │ ├── arc.dart │ ├── arrow.dart │ ├── bubble.dart │ ├── circle.dart │ ├── morph.dart │ ├── path.dart │ ├── polygon.dart │ ├── rectangle.dart │ ├── rounded_rectangle.dart │ ├── star.dart │ ├── trapezoid.dart │ └── triangle.dart │ ├── ui_data_classes │ ├── corner_style.dart │ ├── dynamic_border_radius.dart │ ├── dynamic_border_side.dart │ ├── dynamic_offset.dart │ ├── dynamic_rectangle_styles.dart │ ├── shape_shadow.dart │ └── shape_side.dart │ └── utils │ ├── utils_extension_methods.dart │ └── utils_math_geometry.dart ├── pubspec.lock ├── pubspec.yaml └── test └── morphable_shape_test.dart /.github/workflows/firebase-hosting-pull-request.yml: -------------------------------------------------------------------------------- 1 | # This file was auto-generated by the Firebase CLI 2 | # https://github.com/firebase/firebase-tools 3 | 4 | name: Deploy to Firebase Hosting on PR 5 | 'on': pull_request 6 | jobs: 7 | build_and_preview: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v2 11 | - uses: FirebaseExtended/action-hosting-deploy@v0 12 | with: 13 | repoToken: '${{ secrets.GITHUB_TOKEN }}' 14 | firebaseServiceAccount: '${{ secrets.FIREBASE_SERVICE_ACCOUNT_FLUTTER_SHAPE_2B19E }}' 15 | projectId: flutter-shape-2b19e 16 | env: 17 | FIREBASE_CLI_PREVIEWS: hostingchannels 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | 12 | # IntelliJ related 13 | *.iml 14 | *.ipr 15 | *.iws 16 | .idea/ 17 | 18 | # The .vscode folder contains launch configuration and tasks you configure in 19 | # VS Code which you may wish to be included in version control, so this line 20 | # is commented out by default. 21 | #.vscode/ 22 | 23 | # Flutter/Dart/Pub related 24 | **/doc/api/ 25 | .dart_tool/ 26 | .flutter-plugins 27 | .flutter-plugins-dependencies 28 | .packages 29 | .pub-cache/ 30 | .pub/ 31 | build/ 32 | 33 | # Android related 34 | **/android/**/gradle-wrapper.jar 35 | **/android/.gradle 36 | **/android/captures/ 37 | **/android/gradlew 38 | **/android/gradlew.bat 39 | **/android/local.properties 40 | **/android/**/GeneratedPluginRegistrant.java 41 | 42 | # iOS/XCode related 43 | **/ios/**/*.mode1v3 44 | **/ios/**/*.mode2v3 45 | **/ios/**/*.moved-aside 46 | **/ios/**/*.pbxuser 47 | **/ios/**/*.perspectivev3 48 | **/ios/**/*sync/ 49 | **/ios/**/.sconsign.dblite 50 | **/ios/**/.tags* 51 | **/ios/**/.vagrant/ 52 | **/ios/**/DerivedData/ 53 | **/ios/**/Icon? 54 | **/ios/**/Pods/ 55 | **/ios/**/.symlinks/ 56 | **/ios/**/profile 57 | **/ios/**/xcuserdata 58 | **/ios/.generated/ 59 | **/ios/Flutter/App.framework 60 | **/ios/Flutter/Flutter.framework 61 | **/ios/Flutter/Flutter.podspec 62 | **/ios/Flutter/Generated.xcconfig 63 | **/ios/Flutter/app.flx 64 | **/ios/Flutter/app.zip 65 | **/ios/Flutter/flutter_assets/ 66 | **/ios/Flutter/flutter_export_environment.sh 67 | **/ios/ServiceDefinitions.json 68 | **/ios/Runner/GeneratedPluginRegistrant.* 69 | 70 | # Exceptions to above rules. 71 | !**/ios/**/default.mode1v3 72 | !**/ios/**/default.mode2v3 73 | !**/ios/**/default.pbxuser 74 | !**/ios/**/default.perspectivev3 75 | 76 | **/web/** 77 | **/*firebase* 78 | -------------------------------------------------------------------------------- /.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: d9044a8a61e4d7d0afbd4fe8bad6d40762c289dd 8 | channel: master 9 | 10 | project_type: package 11 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [0.0.1] - TODO: Add release date. 2 | 3 | Add various shapes and the shape morph algorithm. 4 | 5 | ## [0.0.2] - 02.04.2021 6 | 7 | Rewrite the morphing algorithm to be more accurate and efficient. 8 | Update the shape editor. 9 | 10 | ## [0.0.3] - 02.04.2021 11 | 12 | Fix control point drag lag when editing freeform shapes 13 | 14 | ## [0.0.4] - 02.04.2021 15 | 16 | Lets you select morph method 17 | 18 | ## [0.0.5] - 02.05.2021 19 | 20 | Fixed some static code warnings 21 | 22 | ## [0.0.6] - 02.05.2021 23 | 24 | Update package README 25 | 26 | ## [0.0.7] - 02.06.2021 27 | 28 | Update package README 29 | 30 | ## [0.0.8] - 02.06.2021 31 | 32 | Update package README 33 | 34 | ## [0.0.9] - 02.06.2021 35 | 36 | Update package structure 37 | 38 | ## [1.0.0] - 02.06.2021 39 | 40 | Update the auto morph method. First stable version ready. 41 | 42 | ## [1.0.1] - 02.06.2021 43 | 44 | Update the auto morph method. More intuitive. 45 | 46 | ## [1.0.2] - 02.07.2021 47 | 48 | Update api due to length_unit update. 49 | 50 | ## [1.0.3] - 02.07.2021 51 | 52 | Use type instead of name in toJson() methods. 53 | 54 | ## [1.0.4] - 02.07.2021 55 | 56 | Rename DynamicMaterial to ShadowedShape 57 | 58 | ## [1.0.5] - 02.08.2021 59 | 60 | Add animated shadowed shape 61 | 62 | ## [1.0.6] - 02.08.2021 63 | 64 | Bug fix for animated shadowed shape 65 | 66 | ## [1.0.8-nullsafety] - 02.08.2021 67 | 68 | Null safety 69 | 70 | ## [1.0.9-nullsafety] - 02.08.2021 71 | 72 | Update the auto morph method. 73 | 74 | ## [1.1.0-nullsafety] - 02.20.2021 75 | 76 | Performance boost on the morphing process. 77 | 78 | Support for multi-colored border and gradient 79 | filled border. 80 | 81 | Update to the editing tool. 82 | 83 | ## [1.1.0] - 02.20.2021 84 | 85 | Null safety 86 | 87 | ## [1.1.1] - 02.20.2021 88 | 89 | sdk version changed 90 | 91 | ## [1.1.2] - 02.21.2021 92 | 93 | code cleanup 94 | 95 | ## [1.1.3] - 02.21.2021 96 | 97 | Update morph algorithm 98 | 99 | ## [1.1.4] - 02.24.2021 100 | 101 | Border side go back to use double as width for constraint reasons 102 | 103 | ## [1.1.5] - 02.25.2021 104 | 105 | Update drawBorder 106 | 107 | ## [1.1.6] - 02.25.2021 108 | 109 | Avoid morph shape nest into another morph shape 110 | 111 | ## [1.1.7] - 02.25.2021 112 | 113 | Update length_unit package to dimension 114 | 115 | ## [1.1.8] - 02.25.2021 116 | 117 | Update length_unit package to dimension 118 | 119 | ## [1.1.9] - 02.25.2021 120 | 121 | Update length_unit package to dimension 122 | 123 | ## [1.2.0] - 02.27.2021 124 | 125 | ShapeShadow class - supports gradient shadow for shapes 126 | 127 | ## [1.2.1] - 02.28.2021 128 | 129 | ShapeShadow class - supports spread radius 130 | 131 | ## [1.2.2] - 03.07.2021 132 | 133 | Handle BorderStyle.none correctly now 134 | 135 | ## [1.2.3] - 03.09.2021 136 | 137 | PathShape parsing fixed 138 | 139 | ## [1.2.4] - 03.09.2021 140 | 141 | Update dependence 142 | 143 | ## [1.2.5] - 03.09.2021 144 | 145 | fix int to double problem 146 | 147 | ## [1.2.6] - 03.11.2021 148 | 149 | Update dependence 150 | 151 | ## [1.2.7] - 03.11.2021 152 | 153 | Update to the morphing algorithm, now the same shape class 154 | should morph in a more controlled manner 155 | 156 | ## [1.2.8] - 03.12.2021 157 | 158 | Update to the morphing algorithm. 159 | 160 | ## [1.2.9] - 03.12.2021 161 | 162 | Update to the morphing algorithm. 163 | 164 | ## [1.3.0] - 03.12.2021 165 | 166 | Update to the morphing algorithm. 167 | 168 | ## [1.3.1] - 03.13.2021 169 | 170 | Shadow blur radius not negative 171 | 172 | ## [1.3.2] - 03.16.2021 173 | 174 | Support inset shadows 175 | 176 | ## [1.3.3] - 03.16.2021 177 | 178 | Support decoration for animated shadowed shape 179 | 180 | ## [1.3.4] - 03.20.2021 181 | 182 | Update the decorated shadowed shape class 183 | 184 | ## [1.3.5] - 03.23.2021 185 | 186 | Add support for trim border path 187 | 188 | ## [1.3.6] - 03.29.2021 189 | 190 | Add support for stroke join, stroke cap 191 | Round rect border fill update 192 | Support ShapeDecoration 193 | 194 | ## [1.3.7] - 03.29.2021 195 | 196 | Fix a clamp offset issue 197 | 198 | ## [1.3.8] - 03.29.2021 199 | 200 | Fix a clamp offset issue 201 | 202 | ## [1.3.9] - 03.29.2021 203 | 204 | Fix a border lerp bug 205 | 206 | ## [1.4.0] - 03.29.2021 207 | 208 | Fix a clamp offset issue 209 | 210 | ## [1.4.1] - 03.29.2021 211 | 212 | Round rect border fill update 213 | 214 | ## [1.4.2] - 03.29.2021 215 | 216 | Change default StrokeCap 217 | 218 | ## [1.4.3] - 04.03.2021 219 | 220 | Remove the Shape class, now you should use ShapeBorder directly 221 | 222 | ## [1.4.4] - 04.03.2021 223 | 224 | Update README 225 | 226 | ## [1.4.5] - 04.03.2021 227 | 228 | Simplify type name in parsing 229 | 230 | ## [1.4.6] - 04.04.2021 231 | 232 | Code restructure 233 | 234 | ## [1.4.7] - 04.17.2021 235 | 236 | Update comment 237 | 238 | ## [1.4.8] - 04.18.2021 239 | 240 | Remove shapecorner, update bubble shape 241 | 242 | ## [1.4.9] - 04.21.2021 243 | 244 | Update dependence 245 | 246 | ## [1.5.0] - 04.22.2021 247 | 248 | Update README, rename RectangleBorders to RectangleBorderSides 249 | 250 | ## [1.5.1] - 05.09.2021 251 | 252 | Update license. 253 | 254 | ## [1.5.2] - 02.20.2022 255 | 256 | Update dependency. 257 | 258 | ## [1.5.3] - 02.22.2022 259 | 260 | Now AnimatedDecoratedShadowedShape can tween between color and gradients. 261 | 262 | ## [1.5.4] - 02.22.2022 263 | 264 | Now AnimatedDecoratedShadowedShape can tween between color and gradients. 265 | 266 | ## [1.5.5] - 03.11.2022 267 | 268 | Support BlurStyle in ShapeShadow. 269 | 270 | ## [1.5.6] - 03.16.2022 271 | 272 | Support crossfade background image. 273 | 274 | ## [1.5.7] - 03.16.2022 275 | 276 | Fix a bug when crossfade background image. 277 | 278 | ## [1.5.8] - 03.16.2022 279 | 280 | Also crossfade gradient with different type. 281 | 282 | ## [1.5.9] - 03.16.2022 283 | 284 | Fix a bug when crossfade background gradient. 285 | 286 | ## [1.6.0] - 03.21.2022 287 | 288 | Update dependency. 289 | 290 | ## [1.6.1] - 03.22.2022 291 | 292 | Update dependency. 293 | 294 | ## [1.6.2] - 03.22.2022 295 | 296 | Update dependency. 297 | 298 | ## [1.6.3] - 03.22.2022 299 | 300 | Update dependency. 301 | 302 | ## [1.6.4] - 03.22.2022 303 | 304 | Update dependency. 305 | 306 | ## [1.6.5] - 02.04.2023 307 | 308 | Update dependency. 309 | 310 | ## [1.6.6] - 05.12.2023 311 | 312 | Update dependency. 313 | 314 | ## [2.0.0] - 12.12.2024 315 | 316 | Breaking change: Fix: Migrate use of hashValues to Object.hash api which has been the latest standard since Dart 2.14.0 317 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Wenkai Fan 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 | # morphable_shape 2 | 3 | A Flutter package for creating various shapes that are responsive 4 | and can morph between each other. 5 | 6 | Notice: This class uses the [dimension](https://pub.dev/packages/dimension) package but only the px and percent units are respected since ShapeBorder has no access to the build context and the screen size. 7 | 8 | ## Getting Started 9 | 10 | First, you need to create a ShapeBorder instance. The responsive feature means that 11 | you can have a single ShapeBorder instance that adapts to different sizes without you calculating the desired dimensions. For example, the following 12 | code will give you a rounded rectangle border with a 60 px circular radius at the top left corner and a (60 px, 10%) elliptical corner at the bottom right corner. 13 | (for more information of how to use the Length class, see [dimension](https://pub.dev/packages/dimension)). 14 | ```dart 15 | ShapeBorder rectangle=RectangleShapeBorder( 16 | borderRadius: DynamicBorderRadius.only( 17 | topLeft: DynamicRadius.circular(10.toPXLength), 18 | bottomRight: DynamicRadius.elliptical(60.0.toPXLength, 10.0.toPercentLength)) 19 | ); 20 | ``` 21 | 22 | You can create a ShapeBorder that gets used by the Material widget or the ClipPath widget which perform the shape clipping. 23 | 24 | ```dart 25 | Widget widget=Material( 26 | shape: rectangle, 27 | clipBehavior: Clip.antiAlias, 28 | child: Container() 29 | ); 30 | ``` 31 | 32 | Or you can use it in a ShapeDecoration and provide it to the Container/DecoratedBox widget: 33 | ```dart 34 | Decoration decoration = ShapeDecoration(shape: rectangle); 35 | Widget widget = Container( 36 | decoration: decoration 37 | ); 38 | ``` 39 | 40 | This package also has the **DecoratedShadowedShape** widget which lets you define inset shadows which Flutter does not support right now: 41 | ```dart 42 | Widget widget = DecoratedShadowedShape( 43 | shape: shape, 44 | shadows: shadows, 45 | insetShadows: insetShadows, 46 | decoration: decoration, 47 | child: child 48 | ); 49 | ``` 50 | 51 | You can run the example app to create a local shape editing tool to see the various shapes supported by this package (also hosted online at [https://fluttershape.com/](https://fluttershape.com/)). 52 | 53 | ## DynamicBorderSide 54 | 55 | The DynamicBorderSide is an extension to the built-in BorderSide class. It supports gradient filling in addtion to single color filling. It also supports painting partially by specifing the begin, end and offset parameters (e.g. paint only the first half of the border side). You can change the strokeJoin and strokeCap paramter as well. 56 | 57 | This class is used to configure how the borders are painted with different shapes. 58 | 59 | ```dart 60 | DynamicBorderSide( 61 | style: BorderStyle.solid, 62 | width: 1, 63 | color: Colors.red, 64 | gradient: LinearGradient(colors:[Colors.red, Colors.blue]), 65 | begin: 0.toPercentLength, 66 | end: 100.toPercentLength, 67 | offset: 0.toPercentLength, 68 | strokeJoin: StrokeJoin.miter, 69 | strokeCap: StrokeCap.round, 70 | ); 71 | ``` 72 | 73 | ## Supported Shapes 74 | 75 | Currently supported shape borders are: 76 | 77 | ### RectangleShapeBorder 78 | The most powerful and commonly used one should be the RectangleShapeBorder class. It allows to you configure each corner of the rectangle individually or at once. 79 | It also automatically scales all sides so that they don’t overlap (just like what CSS does). The RectangleShapeBorder supports four different corner styles: 80 | ```dart 81 | enum CornerStyle{ 82 | rounded, 83 | concave, 84 | straight, 85 | cutout, 86 | } 87 | ``` 88 | 89 | You can configure the corner styles at once or individually: 90 | ```dart 91 | var cornerStyles=RectangleCornerStyles.all(CornerStyle.rounded); 92 | cornerStyles=RectangleCornerStyles.only( 93 | topLeft: CornerStyle.rounded, 94 | bottomRight: CornerStyle.concave 95 | ); 96 | ``` 97 | 98 | You can also specify the border width, color (or gradient) using the 99 | DynamicBorderSide class: 100 | ```dart 101 | var border=DynamicBorderSide( 102 | width: 10, 103 | gradient: LinearGradient(colors:[Colors.red, Colors.green]), 104 | ); 105 | ``` 106 | 107 | Now you get a fully fledged rectangle: 108 | ``` 109 | ShapeBorder rectangle=RectangleShapeBorder( 110 | borderRadius: 111 | const DynamicBorderRadius.all(DynamicRadius.circular(Length(100))), 112 | cornerStyles: cornerStyles, 113 | border: border, 114 | ); 115 | ``` 116 | 117 | You can make a triangle, a diamond, a trapezoid, or even an arrow shape by just using the RectangleShapeBorder class and providing the right corner style and border radius. 118 | 119 | ![rectangle](https://i.imgur.com/I0jXJu2.png) 120 | 121 | ### CircleShapeBorder 122 | 123 | CircleShapeBorder gives you a circle. Simple as that. 124 | 125 | ## RoundedRectangleShapeBorder 126 | If you use the RoundedRectangleShapeBorder class, then the four border sides can be configured individually. The four border sides can be styled independently or at once, similar to what CSS offers: 127 | 128 | ```dart 129 | var borders=RectangleBorderSides.all(DynamicBorderSide.none); 130 | borders=RectangleBorderSides.symmetric( 131 | horizontal: DynamicBorderSide( 132 | width: 10, 133 | color: Colors.blue, 134 | )); 135 | borders=RectangleBorderSides.only( 136 | top: DynamicBorderSide( 137 | width: 10, 138 | gradient: LinearGradient(colors:[Colors.red, Colors.green]), 139 | )); 140 | ``` 141 | 142 | Then you have: 143 | ```dart 144 | ShapeBorder shapeBorder=RoundedRectangleShapeBorder( 145 | borderRadius: DynamicBorderRadius.all(DynamicRadius.circular(Length(100))), 146 | borderSides: borders, 147 | ); 148 | ``` 149 | Below are some border designs using this class. This class is very similar to what CSS 150 | offers and is a combination of the BoxBorder and RoundedRectangleBorder class that Flutter 151 | offers. 152 | 153 | ![round_rectangle](https://i.imgur.com/Gfh5zxu.png) 154 | 155 | ### PolygonShapeBorder 156 | PolygonShapeBorder supports changing the number of sides as well as corner radius and corner style: 157 | ```dart 158 | PolygonShapeBorder( 159 | sides:6, 160 | cornerRadius: 10.toPercentLength, 161 | cornerStyle: CornerStyle.rounded 162 | ) 163 | ``` 164 | ![polygon](https://i.imgur.com/pzADQHO.png) 165 | 166 | ### StarShapeBorder 167 | The StarShapeBorder allows you to change the number of corners, the inset, the border radius, the border style, the inset radius, and the inset style. 168 | 169 | ```dart 170 | StarShapeBorder( 171 | corners: 5, 172 | inset: 50.toPercentLength, 173 | cornerRadius: 0.toPXLength, 174 | cornerStyle: CornerStyle.rounded, 175 | insetRadius: 0.toPXLength, 176 | insetStyle: CornerStyle.rounded 177 | ) 178 | ``` 179 | ![star](https://i.imgur.com/00JT5jK.png) 180 | 181 | ### ArcShapeBorder, ArrowShapeBorder, BubbleShapeBorder, TrapezoidShapeBorder, TriangleShapeBorder 182 | These shape borders are also supported and responsive. Check out their constructors to see how to make them. 183 | 184 | ![various](https://i.imgur.com/qDK8sBf.png) 185 | 186 | ### PathShapeBorder 187 | Accepts a DynamicPath instance to draw a custom path border. Right now only straight line and cubic Bezier curves are supported. Arcs need to be translated to cubic Beziers first. In the future I may let this class accept close SVG path as well. 188 | 189 | ## Shape Morphing 190 | 191 | Every shape in this package can be morphed into one another, including the border side(s). Hence the name of this package. To morph between two shapes you first need to create a ShapeBorderTween: 192 | ```dart 193 | MorphableShapeBorderTween shapeBorderTween = 194 | MorphableShapeBorderTween(begin: beginShapeBorder, end: endShapeBorder); 195 | ``` 196 | 197 | Then you can get the intermediate shapes at progress **t**(from 0 to 1) by calling: 198 | 199 | ```dart 200 | ShapeBorder intermediate=shapeBorderTween.lerp(t); 201 | ``` 202 | 203 | For an explanation and demonstration of the morphing algorithm, take a look at this 204 | [Medium post](https://kevinvan.medium.com/creating-morphable-shapes-in-flutter-a-complete-rewrite-ac899bfe4222). 205 | 206 | Below are some showcases of the shape morphing process. Most shapes can be morphed in a natural fashion. 207 | 208 | ![morph](https://i.imgur.com/Ic9xJeN.gif) 209 | 210 | ![morph2](https://i.imgur.com/j7k4wL6.gif) 211 | 212 | There are three morph methods to choose when you create your tween. 213 | ```dart 214 | MorphableShapeBorderTween shapeBorderTween = 215 | MorphableShapeBorderTween(begin: startBorder, end: endBorder, 216 | method: MorphMethod.auto); 217 | ``` 218 | The MorphMethod.weighted tries to use as little control 219 | points as possible to do the morphing and takes into account the length of each side of a shape. The MorphMethod.unweighted uses more points 220 | but do not utilize the length information. The MorphMethod.auto will choose either 221 | one of the two methods based on some geometric criteria to make the morphing 222 | process to look more natural. The auto method generally works well, but you can 223 | try other ones if the morphing looks weird. 224 | 225 | Shapes with the same geometry can be morphed faster and in a more consistent way. For example, rectangles and rounded rectangles, or polygons with the same number of sides. For other shapes, the morphing takes more time and may not look great especially for two very distinct shapes. 226 | 227 | ## Shape Serialization 228 | 229 | Every shape in this package supports serialization. If you have designed some shape you like, just call toJson() on it. Then you can reuse it by writting. 230 | 231 | ```dart 232 | Shape shape=RoundedRectangleShape(); 233 | String jsonStr=json.encode(shape.toJson()); 234 | MorphableShapeBorder shapeDecoded=parseMorphableShapeBorder(json.decode(jsonStr)); 235 | ``` 236 | 237 | ## Decorated Shadowed Shape 238 | You can use the DecoratedShadowedShape widget to add shadow and inset shadow to your widget. 239 | ```dart 240 | DecoratedShadowedShape( 241 | shape: shape, 242 | shadows: shadows, 243 | insetShadows: insetShadows, 244 | decoration: decoration, 245 | child: child); 246 | ``` 247 | This will render the following component from bottom to top: shadows, decoration, inset shadows, child, shape border. 248 | 249 | The ShapeShadow is very similar to the BoxShadow class but supports gradient filling: 250 | ```dart 251 | const ShapeShadow({ 252 | Color color = const Color(0xFF000000), 253 | Offset offset = Offset.zero, 254 | double blurRadius = 0.0, 255 | this.spreadRadius = 0.0, 256 | this.gradient, 257 | }) 258 | ``` 259 | 260 | If you want implicit animation for this widget, use the AnimatedDecoratedShadowedShape. 261 | 262 | The [animated_styled_widget](https://pub.dev/packages/animated_styled_widget) has many exmaples using the DecoratedShadowedShape widget. 263 | 264 | ## A Shape Editing Tool 265 | 266 | As I mentioned before, the example app in this package is a shape editing tool for 267 | you to quickly design the shape you want and generate the corresponding code. Below are 268 | some screenshots of the interfaces of this tool. I put about equal amount of effort 269 | into developing this tool compared to developing this package so I strongly recommend you 270 | to check it out. You can either build it locally or visit: 271 | [https://fluttershape.com/](https://fluttershape.com/). 272 | 273 | ![interface](https://i.imgur.com/MhMABhT.png) 274 | ![interface2](https://i.imgur.com/cLyVdrW.png) 275 | ![interface3](https://i.imgur.com/ZaJGegQ.png) 276 | ![interface4](https://i.imgur.com/oXCRr02.png) 277 | ![interface5](https://i.imgur.com/aRRjDdh.png) 278 | -------------------------------------------------------------------------------- /analysis_option.yaml: -------------------------------------------------------------------------------- 1 | include: package:pedantic/analysis_options.1.9.0.yaml -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.lock 4 | *.log 5 | *.pyc 6 | *.swp 7 | .DS_Store 8 | .atom/ 9 | .buildlog/ 10 | .history 11 | .svn/ 12 | 13 | # IntelliJ related 14 | *.iml 15 | *.ipr 16 | *.iws 17 | .idea/ 18 | 19 | # Visual Studio Code related 20 | .vscode/ 21 | 22 | # Flutter/Dart/Pub related 23 | **/doc/api/ 24 | .dart_tool/ 25 | .flutter-plugins 26 | .packages 27 | .pub-cache/ 28 | .pub/ 29 | build/ 30 | 31 | # Android related 32 | **/android/**/gradle-wrapper.jar 33 | **/android/.gradle 34 | **/android/captures/ 35 | **/android/gradlew 36 | **/android/gradlew.bat 37 | **/android/local.properties 38 | **/android/**/GeneratedPluginRegistrant.java 39 | 40 | # iOS/XCode related 41 | **/ios/**/*.mode1v3 42 | **/ios/**/*.mode2v3 43 | **/ios/**/*.moved-aside 44 | **/ios/**/*.pbxuser 45 | **/ios/**/*.perspectivev3 46 | **/ios/**/*sync/ 47 | **/ios/**/.sconsign.dblite 48 | **/ios/**/.tags* 49 | **/ios/**/.vagrant/ 50 | **/ios/**/DerivedData/ 51 | **/ios/**/Icon? 52 | **/ios/**/Pods/ 53 | **/ios/**/.symlinks/ 54 | **/ios/**/profile 55 | **/ios/**/xcuserdata 56 | **/ios/.generated/ 57 | **/ios/Flutter/App.framework 58 | **/ios/Flutter/Flutter.framework 59 | **/ios/Flutter/Generated.xcconfig 60 | **/ios/Flutter/app.flx 61 | **/ios/Flutter/app.zip 62 | **/ios/Flutter/flutter_assets/ 63 | **/ios/ServiceDefinitions.json 64 | **/ios/Runner/GeneratedPluginRegistrant.* 65 | 66 | # Exceptions to above rules. 67 | !**/ios/**/default.mode1v3 68 | !**/ios/**/default.mode2v3 69 | !**/ios/**/default.pbxuser 70 | !**/ios/**/default.perspectivev3 71 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages 72 | -------------------------------------------------------------------------------- /example/.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: 985ccb6d14c6ce5ce74823a4d366df2438eac44f 8 | channel: beta 9 | 10 | project_type: app 11 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # example 2 | 3 | A new Flutter project. 4 | 5 | ## Getting Started 6 | 7 | This project is a starting point for a Flutter application. 8 | 9 | A few resources to get you started if this is your first Flutter project: 10 | 11 | - [Lab: Write your first Flutter app](https://flutter.dev/docs/get-started/codelab) 12 | - [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook) 13 | 14 | For help getting started with Flutter, view our 15 | [online documentation](https://flutter.dev/docs), which offers tutorials, 16 | samples, guidance on mobile development, and a full API reference. 17 | -------------------------------------------------------------------------------- /example/android/.gitignore: -------------------------------------------------------------------------------- 1 | gradle-wrapper.jar 2 | /.gradle 3 | /captures/ 4 | /gradlew 5 | /gradlew.bat 6 | /local.properties 7 | GeneratedPluginRegistrant.java 8 | 9 | # Remember to never publicly share your keystore. 10 | # See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app 11 | key.properties 12 | -------------------------------------------------------------------------------- /example/android/android.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KevinVan720/morphable_shape/f0409e3fd0f001ccd2741edb3ce27049404fbdb3/example/android/android.zip -------------------------------------------------------------------------------- /example/android/app/build.gradle: -------------------------------------------------------------------------------- 1 | def localProperties = new Properties() 2 | def localPropertiesFile = rootProject.file('local.properties') 3 | if (localPropertiesFile.exists()) { 4 | localPropertiesFile.withReader('UTF-8') { reader -> 5 | localProperties.load(reader) 6 | } 7 | } 8 | 9 | def flutterRoot = localProperties.getProperty('flutter.sdk') 10 | if (flutterRoot == null) { 11 | throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") 12 | } 13 | 14 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode') 15 | if (flutterVersionCode == null) { 16 | flutterVersionCode = '1' 17 | } 18 | 19 | def flutterVersionName = localProperties.getProperty('flutter.versionName') 20 | if (flutterVersionName == null) { 21 | flutterVersionName = '1.0' 22 | } 23 | 24 | apply plugin: 'com.android.application' 25 | apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" 26 | 27 | android { 28 | compileSdkVersion 28 29 | 30 | lintOptions { 31 | disable 'InvalidPackage' 32 | } 33 | 34 | defaultConfig { 35 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 36 | applicationId "com.example.example" 37 | minSdkVersion 28 38 | targetSdkVersion 29 39 | versionCode flutterVersionCode.toInteger() 40 | versionName flutterVersionName 41 | testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' 42 | } 43 | 44 | buildTypes { 45 | release { 46 | // TODO: Add your own signing config for the release build. 47 | // Signing with the debug keys for now, so `flutter run --release` works. 48 | signingConfig signingConfigs.debug 49 | } 50 | } 51 | } 52 | 53 | flutter { 54 | source '../..' 55 | } 56 | 57 | dependencies { 58 | testImplementation 'junit:junit:4.12' 59 | androidTestImplementation 'androidx.test.ext:junit:1.1.1' 60 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.0' 61 | } 62 | -------------------------------------------------------------------------------- /example/android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 8 | 9 | 10 | 15 | 18 | 21 | 28 | 32 | 35 | 36 | 37 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /example/android/app/src/main/java/com/example/example/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.example.example; 2 | 3 | import io.flutter.embedding.android.FlutterActivity; 4 | 5 | public class MainActivity extends FlutterActivity { 6 | 7 | } 8 | -------------------------------------------------------------------------------- /example/android/app/src/main/kotlin/com/example/example/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.example.example 2 | 3 | import io.flutter.embedding.android.FlutterActivity 4 | 5 | class MainActivity: FlutterActivity() { 6 | } 7 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/drawable-v21/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KevinVan720/morphable_shape/f0409e3fd0f001ccd2741edb3ce27049404fbdb3/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KevinVan720/morphable_shape/f0409e3fd0f001ccd2741edb3ce27049404fbdb3/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KevinVan720/morphable_shape/f0409e3fd0f001ccd2741edb3ce27049404fbdb3/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KevinVan720/morphable_shape/f0409e3fd0f001ccd2741edb3ce27049404fbdb3/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KevinVan720/morphable_shape/f0409e3fd0f001ccd2741edb3ce27049404fbdb3/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 11 | 12 | -------------------------------------------------------------------------------- /example/android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | google() 4 | jcenter() 5 | } 6 | 7 | dependencies { 8 | classpath 'com.android.tools.build:gradle:4.1.2' 9 | } 10 | } 11 | 12 | allprojects { 13 | repositories { 14 | google() 15 | jcenter() 16 | } 17 | } 18 | 19 | rootProject.buildDir = '../build' 20 | subprojects { 21 | project.buildDir = "${rootProject.buildDir}/${project.name}" 22 | } 23 | subprojects { 24 | project.evaluationDependsOn(':app') 25 | } 26 | 27 | task clean(type: Delete) { 28 | delete rootProject.buildDir 29 | } 30 | -------------------------------------------------------------------------------- /example/android/gradle.properties: -------------------------------------------------------------------------------- 1 | android.enableJetifier=true 2 | android.useAndroidX=true 3 | org.gradle.jvmargs=-Xmx1536M 4 | -------------------------------------------------------------------------------- /example/android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sun Jan 31 12:14:38 EST 2021 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-bin.zip 7 | -------------------------------------------------------------------------------- /example/android/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | 3 | def flutterProjectRoot = rootProject.projectDir.parentFile.toPath() 4 | 5 | def plugins = new Properties() 6 | def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins') 7 | if (pluginsFile.exists()) { 8 | pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) } 9 | } 10 | 11 | plugins.each { name, path -> 12 | def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile() 13 | include ":$name" 14 | project(":$name").projectDir = pluginDirectory 15 | } 16 | -------------------------------------------------------------------------------- /example/assets/images/Icon-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KevinVan720/morphable_shape/f0409e3fd0f001ccd2741edb3ce27049404fbdb3/example/assets/images/Icon-192.png -------------------------------------------------------------------------------- /example/assets/images/Icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KevinVan720/morphable_shape/f0409e3fd0f001ccd2741edb3ce27049404fbdb3/example/assets/images/Icon-512.png -------------------------------------------------------------------------------- /example/ios/.gitignore: -------------------------------------------------------------------------------- 1 | *.mode1v3 2 | *.mode2v3 3 | *.moved-aside 4 | *.pbxuser 5 | *.perspectivev3 6 | **/*sync/ 7 | .sconsign.dblite 8 | .tags* 9 | **/.vagrant/ 10 | **/DerivedData/ 11 | Icon? 12 | **/Pods/ 13 | **/.symlinks/ 14 | profile 15 | xcuserdata 16 | **/.generated/ 17 | Flutter/App.framework 18 | Flutter/Flutter.framework 19 | Flutter/Flutter.podspec 20 | Flutter/Generated.xcconfig 21 | Flutter/app.flx 22 | Flutter/app.zip 23 | Flutter/flutter_assets/ 24 | Flutter/flutter_export_environment.sh 25 | ServiceDefinitions.json 26 | Runner/GeneratedPluginRegistrant.* 27 | 28 | # Exceptions to above rules. 29 | !default.mode1v3 30 | !default.mode2v3 31 | !default.pbxuser 32 | !default.perspectivev3 33 | -------------------------------------------------------------------------------- /example/ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | App 9 | CFBundleIdentifier 10 | io.flutter.flutter.app 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | App 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.0 23 | MinimumOSVersion 24 | 8.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /example/ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Generated.xcconfig" 2 | -------------------------------------------------------------------------------- /example/ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Generated.xcconfig" 2 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 54 | 56 | 62 | 63 | 64 | 65 | 66 | 67 | 73 | 75 | 81 | 82 | 83 | 84 | 86 | 87 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/ios/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Flutter 3 | 4 | @UIApplicationMain 5 | @objc class AppDelegate: FlutterAppDelegate { 6 | override func application( 7 | _ application: UIApplication, 8 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 9 | ) -> Bool { 10 | GeneratedPluginRegistrant.register(with: self) 11 | return super.application(application, didFinishLaunchingWithOptions: launchOptions) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "Icon-App-20x20@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "Icon-App-20x20@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "Icon-App-29x29@1x.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "Icon-App-29x29@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "29x29", 29 | "idiom" : "iphone", 30 | "filename" : "Icon-App-29x29@3x.png", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "Icon-App-40x40@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "40x40", 41 | "idiom" : "iphone", 42 | "filename" : "Icon-App-40x40@3x.png", 43 | "scale" : "3x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "Icon-App-60x60@2x.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "60x60", 53 | "idiom" : "iphone", 54 | "filename" : "Icon-App-60x60@3x.png", 55 | "scale" : "3x" 56 | }, 57 | { 58 | "size" : "20x20", 59 | "idiom" : "ipad", 60 | "filename" : "Icon-App-20x20@1x.png", 61 | "scale" : "1x" 62 | }, 63 | { 64 | "size" : "20x20", 65 | "idiom" : "ipad", 66 | "filename" : "Icon-App-20x20@2x.png", 67 | "scale" : "2x" 68 | }, 69 | { 70 | "size" : "29x29", 71 | "idiom" : "ipad", 72 | "filename" : "Icon-App-29x29@1x.png", 73 | "scale" : "1x" 74 | }, 75 | { 76 | "size" : "29x29", 77 | "idiom" : "ipad", 78 | "filename" : "Icon-App-29x29@2x.png", 79 | "scale" : "2x" 80 | }, 81 | { 82 | "size" : "40x40", 83 | "idiom" : "ipad", 84 | "filename" : "Icon-App-40x40@1x.png", 85 | "scale" : "1x" 86 | }, 87 | { 88 | "size" : "40x40", 89 | "idiom" : "ipad", 90 | "filename" : "Icon-App-40x40@2x.png", 91 | "scale" : "2x" 92 | }, 93 | { 94 | "size" : "76x76", 95 | "idiom" : "ipad", 96 | "filename" : "Icon-App-76x76@1x.png", 97 | "scale" : "1x" 98 | }, 99 | { 100 | "size" : "76x76", 101 | "idiom" : "ipad", 102 | "filename" : "Icon-App-76x76@2x.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "83.5x83.5", 107 | "idiom" : "ipad", 108 | "filename" : "Icon-App-83.5x83.5@2x.png", 109 | "scale" : "2x" 110 | }, 111 | { 112 | "size" : "1024x1024", 113 | "idiom" : "ios-marketing", 114 | "filename" : "Icon-App-1024x1024@1x.png", 115 | "scale" : "1x" 116 | } 117 | ], 118 | "info" : { 119 | "version" : 1, 120 | "author" : "xcode" 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KevinVan720/morphable_shape/f0409e3fd0f001ccd2741edb3ce27049404fbdb3/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KevinVan720/morphable_shape/f0409e3fd0f001ccd2741edb3ce27049404fbdb3/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KevinVan720/morphable_shape/f0409e3fd0f001ccd2741edb3ce27049404fbdb3/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KevinVan720/morphable_shape/f0409e3fd0f001ccd2741edb3ce27049404fbdb3/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KevinVan720/morphable_shape/f0409e3fd0f001ccd2741edb3ce27049404fbdb3/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KevinVan720/morphable_shape/f0409e3fd0f001ccd2741edb3ce27049404fbdb3/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KevinVan720/morphable_shape/f0409e3fd0f001ccd2741edb3ce27049404fbdb3/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KevinVan720/morphable_shape/f0409e3fd0f001ccd2741edb3ce27049404fbdb3/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KevinVan720/morphable_shape/f0409e3fd0f001ccd2741edb3ce27049404fbdb3/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KevinVan720/morphable_shape/f0409e3fd0f001ccd2741edb3ce27049404fbdb3/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KevinVan720/morphable_shape/f0409e3fd0f001ccd2741edb3ce27049404fbdb3/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KevinVan720/morphable_shape/f0409e3fd0f001ccd2741edb3ce27049404fbdb3/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KevinVan720/morphable_shape/f0409e3fd0f001ccd2741edb3ce27049404fbdb3/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KevinVan720/morphable_shape/f0409e3fd0f001ccd2741edb3ce27049404fbdb3/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KevinVan720/morphable_shape/f0409e3fd0f001ccd2741edb3ce27049404fbdb3/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "LaunchImage.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "LaunchImage@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "LaunchImage@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KevinVan720/morphable_shape/f0409e3fd0f001ccd2741edb3ce27049404fbdb3/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KevinVan720/morphable_shape/f0409e3fd0f001ccd2741edb3ce27049404fbdb3/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KevinVan720/morphable_shape/f0409e3fd0f001ccd2741edb3ce27049404fbdb3/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md: -------------------------------------------------------------------------------- 1 | # Launch Screen Assets 2 | 3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory. 4 | 5 | You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. -------------------------------------------------------------------------------- /example/ios/Runner/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /example/ios/Runner/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /example/ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | example 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | $(FLUTTER_BUILD_NAME) 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(FLUTTER_BUILD_NUMBER) 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIMainStoryboardFile 28 | Main 29 | UISupportedInterfaceOrientations 30 | 31 | UIInterfaceOrientationPortrait 32 | UIInterfaceOrientationLandscapeLeft 33 | UIInterfaceOrientationLandscapeRight 34 | 35 | UISupportedInterfaceOrientations~ipad 36 | 37 | UIInterfaceOrientationPortrait 38 | UIInterfaceOrientationPortraitUpsideDown 39 | UIInterfaceOrientationLandscapeLeft 40 | UIInterfaceOrientationLandscapeRight 41 | 42 | UIViewControllerBasedStatusBarAppearance 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /example/ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" 2 | -------------------------------------------------------------------------------- /example/lib/how_to_use_text.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | const List howToTextWidgets = [ 4 | Text("Welcome to Flutter shape editor!", style: TextStyle(fontSize: 20)), 5 | Divider(), 6 | Text("Double click to enable/disable shape editing."), 7 | Divider(), 8 | Text( 9 | "Resize the shape by dragging the four handles when shape editing is disabled."), 10 | Divider(), 11 | Text( 12 | '''Drag the various handles on the shape to change shape properties (or edit their values directly in the side panel) when in shape editing mode.'''), 13 | Divider(), 14 | Text( 15 | "Click the To Bezier button to convert the shape to a freeform path shape."), 16 | Divider(), 17 | Text("Click the shape icon button to choose other shapes."), 18 | Divider(), 19 | Text( 20 | "Click the code icon button to see the JSON representation of the current shape."), 21 | Divider(), 22 | Text( 23 | "Click the eye icon button at the top left corner to see the current shape morph between other predefined shapes."), 24 | Divider(), 25 | Text( 26 | "If the morphing process is lagging, try turn off the control points. Painting hundreds of small circles is heavy for the raster thread"), 27 | ]; 28 | -------------------------------------------------------------------------------- /example/lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:dimension/dimension.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | import 'edit_shape_page.dart'; 5 | 6 | void main() => runApp(MyApp()); 7 | 8 | class MyApp extends StatelessWidget { 9 | // This widget is the root of your application. 10 | @override 11 | Widget build(BuildContext context) { 12 | return MaterialApp( 13 | title: 'Flutter Shape Editor', 14 | theme: ThemeData( 15 | primarySwatch: Colors.amber, 16 | primaryColor: Colors.amber, 17 | sliderTheme: SliderTheme.of(context).copyWith( 18 | inactiveTrackColor: Colors.black.withOpacity(0.2), 19 | thumbColor: Colors.amber, 20 | activeTrackColor: Colors.amber, 21 | overlayColor: Colors.amber.withOpacity(0.2), 22 | ), 23 | ), 24 | home: EditShapePage(), 25 | ); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /example/lib/shape_picker.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:morphable_shape/morphable_shape.dart'; 5 | 6 | bool useWhiteForeground(Color color) { 7 | return 1.05 / (color.computeLuminance() + 0.05) > 4.5; 8 | } 9 | 10 | typedef PickerLayoutBuilder = Widget Function(BuildContext context, 11 | List allShape, PickerItem child); 12 | typedef PickerItem = Widget Function(MorphableShapeBorder shape); 13 | typedef PickerItemBuilder = Widget Function( 14 | MorphableShapeBorder shape, 15 | bool isCurrentShape, 16 | void Function() changeShape, 17 | ); 18 | 19 | class BlockShapePicker extends StatefulWidget { 20 | const BlockShapePicker({ 21 | @required this.onShapeChanged, 22 | this.itemBuilder = defaultItemBuilder, 23 | }); 24 | 25 | final Function onShapeChanged; 26 | final PickerItemBuilder itemBuilder; 27 | 28 | static Widget defaultItemBuilder(MorphableShapeBorder shape, 29 | bool isCurrentShape, void Function() changeShape) { 30 | return Material( 31 | clipBehavior: Clip.antiAlias, 32 | type: MaterialType.canvas, 33 | elevation: 1, 34 | shape: shape, 35 | child: Container( 36 | color: isCurrentShape 37 | ? Colors.black.withOpacity(0.7) 38 | : Colors.black.withOpacity(0.1), 39 | child: InkWell( 40 | onTap: changeShape, 41 | radius: 60, 42 | child: Container(), 43 | ), 44 | ), 45 | ); 46 | } 47 | 48 | @override 49 | State createState() => _BlockShapePickerState(); 50 | } 51 | 52 | class _BlockShapePickerState extends State { 53 | MorphableShapeBorder _currentShape; 54 | 55 | @override 56 | void initState() { 57 | _currentShape = presetRoundedRectangleShapeMap["RectangleAll0"]; 58 | super.initState(); 59 | } 60 | 61 | void changeShape(MorphableShapeBorder shape) { 62 | setState(() { 63 | _currentShape = shape; 64 | }); 65 | widget.onShapeChanged(shape); 66 | } 67 | 68 | @override 69 | Widget build(BuildContext context) { 70 | Orientation orientation = MediaQuery.of(context).orientation; 71 | Size screenSize = MediaQuery.of(context).size; 72 | 73 | return Container( 74 | width: min(screenSize.width * 0.8, 360.0), 75 | height: min(screenSize.height * 0.8, 360.0), 76 | child: ListView( 77 | children: presetShapeMap.keys 78 | .map((category) => Column( 79 | crossAxisAlignment: CrossAxisAlignment.start, 80 | children: [ 81 | Container( 82 | alignment: Alignment.centerLeft, 83 | padding: EdgeInsets.only(left: 3, top: 1, bottom: 1), 84 | //color: Colors.grey.withOpacity(0.2), 85 | child: Text( 86 | category, 87 | style: TextStyle(fontWeight: FontWeight.bold), 88 | )), 89 | GridView.count( 90 | physics: NeverScrollableScrollPhysics(), 91 | shrinkWrap: true, 92 | crossAxisCount: 93 | (min(screenSize.width * 0.8, 360.0) / 50).floor(), 94 | crossAxisSpacing: 15.0, 95 | mainAxisSpacing: 15.0, 96 | padding: 97 | EdgeInsets.symmetric(horizontal: 5, vertical: 5), 98 | children: 99 | presetShapeMap[category].keys.map((String name) { 100 | MorphableShapeBorder shape = 101 | presetShapeMap[category][name]; 102 | return widget.itemBuilder(shape, 103 | shape == _currentShape, () => changeShape(shape)); 104 | }).toList(), 105 | ), 106 | ], 107 | )) 108 | .toList(), 109 | )); 110 | } 111 | } 112 | 113 | class BottomSheetShapePicker extends StatefulWidget { 114 | BottomSheetShapePicker({ 115 | this.headText = "Pick a shape", 116 | this.currentShape, 117 | @required this.valueChanged, 118 | }); 119 | 120 | final String headText; 121 | final MorphableShapeBorder currentShape; 122 | final ValueChanged valueChanged; 123 | 124 | @override 125 | _BottomSheetShapePicker createState() => _BottomSheetShapePicker(); 126 | } 127 | 128 | class _BottomSheetShapePicker extends State { 129 | MorphableShapeBorder currentShape; 130 | 131 | @override 132 | void initState() { 133 | currentShape = widget.currentShape ?? RectangleShapeBorder(); 134 | super.initState(); 135 | } 136 | 137 | void changeShape(MorphableShapeBorder shape) { 138 | setState(() => currentShape = shape); 139 | } 140 | 141 | @override 142 | Widget build(BuildContext context) { 143 | return Container( 144 | alignment: Alignment.centerLeft, 145 | padding: EdgeInsets.all(0), 146 | child: RawMaterialButton( 147 | onPressed: () { 148 | showDialog( 149 | context: context, 150 | builder: (BuildContext context) { 151 | return AlertDialog( 152 | content: SingleChildScrollView( 153 | child: Column( 154 | children: [ 155 | Container( 156 | alignment: Alignment.centerLeft, 157 | padding: EdgeInsets.only(bottom: 10), 158 | child: Text( 159 | widget.headText, 160 | style: TextStyle( 161 | fontSize: 20, fontWeight: FontWeight.bold), 162 | )), 163 | BlockShapePicker( 164 | onShapeChanged: changeShape, 165 | ), 166 | ], 167 | ), 168 | ), 169 | actions: [ 170 | TextButton( 171 | child: const Text( 172 | 'Got it', 173 | ), 174 | onPressed: () { 175 | setState(() { 176 | widget.valueChanged(currentShape); 177 | }); 178 | Navigator.of(context)?.pop(); 179 | }, 180 | ), 181 | ], 182 | ); 183 | }, 184 | ); 185 | }, 186 | child: Icon( 187 | Icons.streetview_outlined, 188 | size: 28, 189 | ), 190 | elevation: 5.0, 191 | constraints: BoxConstraints.tight(Size(24, 24)), 192 | padding: const EdgeInsets.all(0.5), 193 | )); 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /example/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: flutter_shape_editor 2 | description: An online editor for editing various Flutter shapes and morph between them. 3 | 4 | version: 1.0.0+1 5 | 6 | environment: 7 | sdk: ">=2.12.0 <4.0.0" 8 | 9 | dependencies: 10 | flutter: 11 | sdk: flutter 12 | morphable_shape: 13 | path: ../ 14 | 15 | dev_dependencies: 16 | flutter_test: 17 | sdk: flutter 18 | 19 | flutter: 20 | uses-material-design: true 21 | assets: 22 | - assets/images/Icon-512.png 23 | - assets/images/Icon-192.png 24 | 25 | -------------------------------------------------------------------------------- /example/test/widget_test.dart: -------------------------------------------------------------------------------- 1 | // This is a basic Flutter widget test. 2 | // 3 | // To perform an interaction with a widget in your test, use the WidgetTester 4 | // utility that Flutter provides. For example, you can send tap and scroll 5 | // gestures. You can also use WidgetTester to find child widgets in the widget 6 | // tree, read text, and verify that the values of widget properties are correct. 7 | 8 | import 'package:flutter/material.dart'; 9 | import 'package:flutter_test/flutter_test.dart'; 10 | 11 | import '../lib/main.dart'; 12 | 13 | void main() { 14 | testWidgets('Counter increments smoke test', (WidgetTester tester) async { 15 | // Build our app and trigger a frame. 16 | await tester.pumpWidget(MyApp()); 17 | 18 | // Verify that our counter starts at 0. 19 | expect(find.text('0'), findsOneWidget); 20 | expect(find.text('1'), findsNothing); 21 | 22 | // Tap the '+' icon and trigger a frame. 23 | await tester.tap(find.byIcon(Icons.add)); 24 | await tester.pump(); 25 | 26 | // Verify that our counter has incremented. 27 | expect(find.text('0'), findsNothing); 28 | expect(find.text('1'), findsOneWidget); 29 | }); 30 | } 31 | -------------------------------------------------------------------------------- /lib/morphable_shape.dart: -------------------------------------------------------------------------------- 1 | library morphable_shape; 2 | 3 | export 'package:dimension/dimension.dart'; 4 | 5 | export 'preset_shape_map.dart'; 6 | export 'src/animated_decorated_shadowd_shape.dart'; 7 | export 'src/decorated_shadowed_shape.dart'; 8 | export 'src/dynamic_path/dynamic_path.dart'; 9 | export 'src/dynamic_path/dynamic_path_morph.dart'; 10 | export 'src/morphable_shape_border.dart'; 11 | export 'src/morphable_shape_border_tween.dart'; 12 | export 'src/parse_json.dart'; 13 | export 'src/shape_borders/arc.dart'; 14 | export 'src/shape_borders/arrow.dart'; 15 | export 'src/shape_borders/bubble.dart'; 16 | export 'src/shape_borders/circle.dart'; 17 | export 'src/shape_borders/path.dart'; 18 | export 'src/shape_borders/polygon.dart'; 19 | export 'src/shape_borders/rectangle.dart'; 20 | export 'src/shape_borders/rounded_rectangle.dart'; 21 | export 'src/shape_borders/star.dart'; 22 | export 'src/shape_borders/trapezoid.dart'; 23 | export 'src/shape_borders/triangle.dart'; 24 | export 'src/ui_data_classes/corner_style.dart'; 25 | export 'src/ui_data_classes/dynamic_border_radius.dart'; 26 | export 'src/ui_data_classes/dynamic_border_side.dart'; 27 | export 'src/ui_data_classes/dynamic_offset.dart'; 28 | export 'src/ui_data_classes/dynamic_rectangle_styles.dart'; 29 | export 'src/ui_data_classes/shape_shadow.dart'; 30 | export 'src/ui_data_classes/shape_side.dart'; 31 | export 'src/utils/utils_extension_methods.dart'; 32 | export 'src/utils/utils_math_geometry.dart'; 33 | -------------------------------------------------------------------------------- /lib/preset_shape_map.dart: -------------------------------------------------------------------------------- 1 | import 'package:dimension/dimension.dart'; 2 | import 'package:morphable_shape/morphable_shape.dart'; 3 | 4 | ///A collection of predefined shapes, grouped into maps 5 | 6 | const Map> presetShapeMap = { 7 | "Rounded Rectangles": presetRoundedRectangleShapeMap, 8 | "Rectangle Like": presetRectangleShapeMap, 9 | "Circle": presetCircleShapeMap, 10 | "Polygons": presetPolygonShapeMap, 11 | "Stars": presetStarShapeMap, 12 | "Triangles": presetTriangleShapeMap, 13 | "Others": presetOtherShapeMap, 14 | }; 15 | 16 | const Map presetCircleShapeMap = { 17 | "Circle": const CircleShapeBorder(), 18 | }; 19 | 20 | const Map presetRoundedRectangleShapeMap = { 21 | "RectangleAll0": const RoundedRectangleShapeBorder( 22 | borderRadius: DynamicBorderRadius.all(DynamicRadius.zero)), 23 | "RoundRectangleAll10": const RoundedRectangleShapeBorder( 24 | borderRadius: DynamicBorderRadius.all(const DynamicRadius.circular( 25 | const Length(10, unit: LengthUnit.percent)))), 26 | "RoundRectangleAll25": const RoundedRectangleShapeBorder( 27 | borderRadius: DynamicBorderRadius.all(const DynamicRadius.circular( 28 | const Length(25, unit: LengthUnit.percent)))), 29 | "RoundRectangleTLBR25": const RoundedRectangleShapeBorder( 30 | borderRadius: DynamicBorderRadius.only( 31 | topLeft: const DynamicRadius.circular( 32 | const Length(25, unit: LengthUnit.percent)), 33 | bottomRight: const DynamicRadius.circular( 34 | const Length(25, unit: LengthUnit.percent)))), 35 | "RoundRectangleTRBL25": const RoundedRectangleShapeBorder( 36 | borderRadius: DynamicBorderRadius.only( 37 | topRight: const DynamicRadius.circular( 38 | const Length(25, unit: LengthUnit.percent)), 39 | bottomLeft: const DynamicRadius.circular( 40 | const Length(25, unit: LengthUnit.percent)))), 41 | "RoundRectangleTL50BL50BR50": const RoundedRectangleShapeBorder( 42 | borderRadius: DynamicBorderRadius.only( 43 | topLeft: const DynamicRadius.circular( 44 | const Length(50, unit: LengthUnit.percent)), 45 | bottomLeft: const DynamicRadius.circular( 46 | const Length(50, unit: LengthUnit.percent)), 47 | bottomRight: const DynamicRadius.circular( 48 | const Length(50, unit: LengthUnit.percent)))), 49 | "RoundRectangleTL50BL50TR50": const RoundedRectangleShapeBorder( 50 | borderRadius: DynamicBorderRadius.only( 51 | topLeft: const DynamicRadius.circular( 52 | const Length(50, unit: LengthUnit.percent)), 53 | bottomLeft: const DynamicRadius.circular( 54 | const Length(50, unit: LengthUnit.percent)), 55 | topRight: const DynamicRadius.circular( 56 | const Length(50, unit: LengthUnit.percent)))), 57 | }; 58 | 59 | const Map presetRectangleShapeMap = { 60 | "RoundAll25": const RectangleShapeBorder( 61 | cornerStyles: RectangleCornerStyles.all(CornerStyle.rounded), 62 | borderRadius: DynamicBorderRadius.all(const DynamicRadius.circular( 63 | const Length(25, unit: LengthUnit.percent)))), 64 | "CutCornerAll25": const RectangleShapeBorder( 65 | cornerStyles: RectangleCornerStyles.all(CornerStyle.straight), 66 | borderRadius: DynamicBorderRadius.all(const DynamicRadius.circular( 67 | const Length(25, unit: LengthUnit.percent)))), 68 | "CutoutCornerAll25": const RectangleShapeBorder( 69 | cornerStyles: RectangleCornerStyles.all(CornerStyle.cutout), 70 | borderRadius: DynamicBorderRadius.all(const DynamicRadius.circular( 71 | const Length(25, unit: LengthUnit.percent)))), 72 | "ConcaveCornerAll25": const RectangleShapeBorder( 73 | cornerStyles: RectangleCornerStyles.all(CornerStyle.concave), 74 | borderRadius: DynamicBorderRadius.all(const DynamicRadius.circular( 75 | const Length(25, unit: LengthUnit.percent)))), 76 | "DiagonalBottomRight": const RectangleShapeBorder( 77 | cornerStyles: 78 | RectangleCornerStyles.only(bottomRight: CornerStyle.straight), 79 | borderRadius: DynamicBorderRadius.only( 80 | bottomRight: const DynamicRadius.elliptical( 81 | Length(100, unit: LengthUnit.percent), 82 | Length(25, unit: LengthUnit.percent)))), 83 | "DiagonalBottomLeft": const RectangleShapeBorder( 84 | cornerStyles: 85 | RectangleCornerStyles.only(bottomLeft: CornerStyle.straight), 86 | borderRadius: DynamicBorderRadius.only( 87 | bottomLeft: const DynamicRadius.elliptical( 88 | Length(100, unit: LengthUnit.percent), 89 | Length(25, unit: LengthUnit.percent)))), 90 | "ChevronLeft": const RectangleShapeBorder( 91 | cornerStyles: RectangleCornerStyles.only( 92 | bottomLeft: CornerStyle.straight, topLeft: CornerStyle.straight), 93 | borderRadius: DynamicBorderRadius.only( 94 | topLeft: const DynamicRadius.circular( 95 | Length(50, unit: LengthUnit.percent)), 96 | bottomLeft: const DynamicRadius.circular( 97 | Length(50, unit: LengthUnit.percent)))), 98 | "ChevronRight": const RectangleShapeBorder( 99 | cornerStyles: RectangleCornerStyles.only( 100 | bottomRight: CornerStyle.straight, topRight: CornerStyle.straight), 101 | borderRadius: DynamicBorderRadius.only( 102 | topRight: const DynamicRadius.circular( 103 | Length(50, unit: LengthUnit.percent)), 104 | bottomRight: const DynamicRadius.circular( 105 | Length(50, unit: LengthUnit.percent)))), 106 | "Diamond": const RectangleShapeBorder( 107 | cornerStyles: RectangleCornerStyles.all(CornerStyle.straight), 108 | borderRadius: DynamicBorderRadius.all( 109 | const DynamicRadius.circular(Length(50, unit: LengthUnit.percent)))), 110 | "ArcTL": const RectangleShapeBorder( 111 | borderRadius: DynamicBorderRadius.only( 112 | topLeft: const DynamicRadius.circular( 113 | Length(100, unit: LengthUnit.percent)))), 114 | "ArcBR": const RectangleShapeBorder( 115 | borderRadius: DynamicBorderRadius.only( 116 | bottomRight: const DynamicRadius.circular( 117 | Length(100, unit: LengthUnit.percent)))), 118 | "DonutTL": const RectangleShapeBorder( 119 | cornerStyles: 120 | RectangleCornerStyles.only(bottomRight: CornerStyle.concave), 121 | borderRadius: DynamicBorderRadius.only( 122 | topLeft: const DynamicRadius.circular( 123 | Length(100, unit: LengthUnit.percent)), 124 | bottomRight: const DynamicRadius.circular( 125 | Length(50, unit: LengthUnit.percent)))), 126 | }; 127 | 128 | const Map presetPolygonShapeMap = { 129 | "Polygon3": const PolygonShapeBorder(sides: 3), 130 | "Polygon5": const PolygonShapeBorder(sides: 5), 131 | "Polygon6": const PolygonShapeBorder(sides: 6), 132 | "Polygon8": const PolygonShapeBorder(sides: 8), 133 | "Polygon12": const PolygonShapeBorder(sides: 12), 134 | "Polygon5Rounded": const PolygonShapeBorder( 135 | sides: 5, cornerRadius: Length(50, unit: LengthUnit.percent)), 136 | "Polygon6Rounded": const PolygonShapeBorder( 137 | sides: 6, cornerRadius: Length(50, unit: LengthUnit.percent)), 138 | "Polygon8Rounded": const PolygonShapeBorder( 139 | sides: 8, cornerRadius: Length(50, unit: LengthUnit.percent)), 140 | "Polygon6Straight": const PolygonShapeBorder( 141 | cornerStyle: CornerStyle.straight, 142 | sides: 6, 143 | cornerRadius: Length(50, unit: LengthUnit.percent)), 144 | "Polygon6Cutout": const PolygonShapeBorder( 145 | cornerStyle: CornerStyle.cutout, 146 | sides: 6, 147 | cornerRadius: Length(50, unit: LengthUnit.percent)), 148 | "Polygon6Concave": const PolygonShapeBorder( 149 | cornerStyle: CornerStyle.concave, 150 | sides: 6, 151 | cornerRadius: Length(50, unit: LengthUnit.percent)), 152 | }; 153 | 154 | const Map presetStarShapeMap = { 155 | "Star4": const StarShapeBorder(corners: 4), 156 | "Star5": const StarShapeBorder(corners: 5), 157 | "Star6": const StarShapeBorder(corners: 6), 158 | "Star8": const StarShapeBorder(corners: 8), 159 | "Star12": const StarShapeBorder(corners: 12), 160 | "Star4Rounded": const StarShapeBorder( 161 | corners: 4, 162 | cornerRadius: Length(50, unit: LengthUnit.percent), 163 | insetRadius: Length(50, unit: LengthUnit.percent)), 164 | "Star6Rounded": const StarShapeBorder( 165 | corners: 6, 166 | cornerRadius: Length(30, unit: LengthUnit.percent), 167 | insetRadius: Length(30, unit: LengthUnit.percent)), 168 | "Star8Rounded": const StarShapeBorder( 169 | corners: 8, 170 | cornerRadius: Length(10, unit: LengthUnit.percent), 171 | insetRadius: Length(10, unit: LengthUnit.percent)), 172 | }; 173 | 174 | const Map presetTriangleShapeMap = { 175 | "Triangle": const TriangleShapeBorder(), 176 | "TriangleBottom": const TriangleShapeBorder( 177 | point1: const DynamicOffset(const Length(50, unit: LengthUnit.percent), 178 | const Length(0, unit: LengthUnit.percent)), 179 | point2: const DynamicOffset(const Length(100, unit: LengthUnit.percent), 180 | const Length(100, unit: LengthUnit.percent)), 181 | point3: const DynamicOffset(const Length(0, unit: LengthUnit.percent), 182 | const Length(100, unit: LengthUnit.percent))), 183 | "TriangleLeft": const TriangleShapeBorder( 184 | point1: const DynamicOffset(const Length(0, unit: LengthUnit.percent), 185 | const Length(0, unit: LengthUnit.percent)), 186 | point2: const DynamicOffset(const Length(100, unit: LengthUnit.percent), 187 | const Length(0, unit: LengthUnit.percent)), 188 | point3: const DynamicOffset(const Length(0, unit: LengthUnit.percent), 189 | const Length(100, unit: LengthUnit.percent))), 190 | "TriangleRight": const TriangleShapeBorder( 191 | point1: const DynamicOffset(const Length(0, unit: LengthUnit.percent), 192 | const Length(0, unit: LengthUnit.percent)), 193 | point2: const DynamicOffset(const Length(100, unit: LengthUnit.percent), 194 | const Length(0, unit: LengthUnit.percent)), 195 | point3: const DynamicOffset(const Length(100, unit: LengthUnit.percent), 196 | const Length(100, unit: LengthUnit.percent))), 197 | }; 198 | 199 | const Map presetOtherShapeMap = { 200 | "BubbleTop": const BubbleShapeBorder( 201 | side: ShapeSide.top, borderRadius: Length(20, unit: LengthUnit.percent)), 202 | "BubbleBottom": const BubbleShapeBorder( 203 | side: ShapeSide.bottom, 204 | borderRadius: Length(20, unit: LengthUnit.percent)), 205 | "BubbleLeft": const BubbleShapeBorder( 206 | side: ShapeSide.left, borderRadius: Length(20, unit: LengthUnit.percent)), 207 | "BubbleRight": const BubbleShapeBorder( 208 | side: ShapeSide.right, 209 | borderRadius: Length(20, unit: LengthUnit.percent)), 210 | "ArcTop": const ArcShapeBorder( 211 | side: ShapeSide.top, arcHeight: Length(20, unit: LengthUnit.percent)), 212 | "ArcBottom": const ArcShapeBorder( 213 | side: ShapeSide.bottom, arcHeight: Length(20, unit: LengthUnit.percent)), 214 | "ArrowRight": const ArrowShapeBorder(), 215 | "ArrowLeft": const ArrowShapeBorder(side: ShapeSide.left), 216 | "Trapezoid": const TrapezoidShapeBorder(), 217 | "TrapezoidBottom": const TrapezoidShapeBorder(side: ShapeSide.top), 218 | }; 219 | -------------------------------------------------------------------------------- /lib/src/animated_decorated_shadowd_shape.dart: -------------------------------------------------------------------------------- 1 | import 'dart:ui'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:animated_box_decoration/animated_box_decoration.dart'; 5 | import 'package:morphable_shape/morphable_shape.dart'; 6 | import 'package:morphable_shape/src/dynamic_path/dynamic_path_morph.dart'; 7 | 8 | class ListShapeShadowTween extends Tween?> { 9 | ListShapeShadowTween({ 10 | List? begin, 11 | List? end, 12 | }) : super(begin: begin, end: end); 13 | 14 | @override 15 | List? lerp(double t) { 16 | return ShapeShadow.lerpList(begin, end, t); 17 | } 18 | } 19 | 20 | ///An implicitly animated version of the DecoratedShadowedShape widget 21 | 22 | class AnimatedDecoratedShadowedShape extends ImplicitlyAnimatedWidget { 23 | AnimatedDecoratedShadowedShape({ 24 | Key? key, 25 | this.child, 26 | this.shadows, 27 | this.insetShadows, 28 | this.decoration, 29 | this.shape, 30 | this.method, 31 | Curve curve = Curves.linear, 32 | required Duration duration, 33 | VoidCallback? onEnd, 34 | }) : super(key: key, curve: curve, duration: duration, onEnd: onEnd); 35 | 36 | final Widget? child; 37 | final List? shadows; 38 | final List? insetShadows; 39 | final Decoration? decoration; 40 | final ShapeBorder? shape; 41 | final MorphMethod? method; 42 | 43 | @override 44 | _AnimatedShadowedShapeState createState() => _AnimatedShadowedShapeState(); 45 | } 46 | 47 | class _AnimatedShadowedShapeState 48 | extends AnimatedWidgetBaseState { 49 | MorphableShapeBorderTween? _shapeBorderTween; 50 | ListShapeShadowTween? _listShadowTween; 51 | ListShapeShadowTween? _listInsetShadowTween; 52 | DecorationTween? _decorationTween; 53 | 54 | @override 55 | void forEachTween(TweenVisitor visitor) { 56 | _shapeBorderTween = visitor( 57 | _shapeBorderTween, 58 | widget.shape, 59 | (dynamic value) => MorphableShapeBorderTween( 60 | begin: value as MorphableShapeBorder, 61 | method: widget.method ?? MorphMethod.auto)) 62 | as MorphableShapeBorderTween?; 63 | _listShadowTween = visitor( 64 | _listShadowTween, 65 | widget.shadows, 66 | (dynamic value) => 67 | ListShapeShadowTween(begin: value as List)) 68 | as ListShapeShadowTween?; 69 | _listInsetShadowTween = visitor( 70 | _listInsetShadowTween, 71 | widget.insetShadows, 72 | (dynamic value) => 73 | ListShapeShadowTween(begin: value as List)) 74 | as ListShapeShadowTween?; 75 | _decorationTween = visitor( 76 | _decorationTween, 77 | widget.decoration, 78 | (dynamic value) => 79 | SmoothDecorationTween(begin: value as Decoration)) 80 | as DecorationTween; 81 | } 82 | 83 | @override 84 | Widget build(BuildContext context) { 85 | final Animation animation = this.animation; 86 | return DecoratedShadowedShape( 87 | decoration: _decorationTween?.evaluate(animation), 88 | shape: _shapeBorderTween?.evaluate(animation), 89 | shadows: _listShadowTween?.evaluate(animation), 90 | insetShadows: _listInsetShadowTween?.evaluate(animation), 91 | child: widget.child, 92 | ); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /lib/src/common_includes.dart: -------------------------------------------------------------------------------- 1 | export 'dart:math'; 2 | 3 | export 'package:dimension/dimension.dart'; 4 | export 'package:flutter/material.dart'; 5 | export 'package:flutter_class_parser/flutter_class_parser.dart'; 6 | export 'package:morphable_shape/morphable_shape.dart'; 7 | -------------------------------------------------------------------------------- /lib/src/decorated_shadowed_shape.dart: -------------------------------------------------------------------------------- 1 | import 'package:morphable_shape/src/common_includes.dart'; 2 | 3 | ///A widget that contains a decoration, shadows, inset shadows and clipped by a shape border 4 | 5 | class CustomShapeBorderClipper extends CustomClipper { 6 | const CustomShapeBorderClipper({ 7 | required this.shape, 8 | this.textDirection, 9 | }); 10 | 11 | /// The shape border whose outer path this clipper clips to. 12 | final ShapeBorder shape; 13 | 14 | /// The text direction to use for getting the outer path for [shape]. 15 | /// 16 | /// [ShapeBorder]s can depend on the text direction (e.g having a "dent" 17 | /// towards the start of the shape). 18 | final TextDirection? textDirection; 19 | 20 | /// Returns the outer path of [shape] as the clip. 21 | @override 22 | Path getClip(Size size) { 23 | return shape.getOuterPath(Offset.zero & size, textDirection: textDirection); 24 | } 25 | 26 | @override 27 | bool shouldReclip(CustomClipper oldClipper) { 28 | return true; 29 | } 30 | } 31 | 32 | class ClipPathShadow extends StatelessWidget { 33 | final List? shadows; 34 | final CustomClipper clipper; 35 | final Widget? child; 36 | 37 | ClipPathShadow({ 38 | this.shadows, 39 | required this.clipper, 40 | this.child, 41 | }); 42 | 43 | @override 44 | Widget build(BuildContext context) { 45 | return CustomPaint( 46 | painter: this.shadows != null 47 | ? _ClipShapeShadowPainter( 48 | clipper: this.clipper, 49 | shadows: this.shadows, 50 | ) 51 | : null, 52 | child: ClipPath(child: child, clipper: this.clipper), 53 | ); 54 | } 55 | } 56 | 57 | class _ClipShapeShadowPainter extends CustomPainter { 58 | final List? shadows; 59 | final CustomClipper clipper; 60 | 61 | _ClipShapeShadowPainter({required this.shadows, required this.clipper}); 62 | 63 | @override 64 | void paint(Canvas canvas, Size size) { 65 | shadows?.forEach((element) { 66 | Rect rect = Rect.fromLTRB(0, 0, size.width, size.height) 67 | .inflate(element.spreadRadius); 68 | var paint = element.toPaint() 69 | ..shader = element.gradient?.createShader(rect); 70 | var clipPath = 71 | clipper.getClip(rect.size).shift(element.offset + rect.topLeft); 72 | canvas.drawPath(clipPath, paint); 73 | }); 74 | } 75 | 76 | @override 77 | bool shouldRepaint(_ClipShapeShadowPainter oldDelegate) { 78 | return oldDelegate.clipper != clipper || oldDelegate.shadows != shadows; 79 | } 80 | } 81 | 82 | class ClipPathInsetShadow extends StatelessWidget { 83 | final List? shadows; 84 | final CustomClipper clipper; 85 | final Widget? child; 86 | final Decoration? decoration; 87 | 88 | ClipPathInsetShadow({ 89 | this.shadows, 90 | required this.clipper, 91 | this.child, 92 | this.decoration, 93 | }); 94 | 95 | @override 96 | Widget build(BuildContext context) { 97 | Widget rst = CustomPaint( 98 | painter: this.shadows != null 99 | ? _ClipInsetShapeShadowPainter( 100 | clipper: this.clipper, 101 | shadows: this.shadows, 102 | ) 103 | : null, 104 | child: child, 105 | ); 106 | return decoration != null 107 | ? DecoratedBox( 108 | decoration: decoration!, 109 | child: rst, 110 | ) 111 | : rst; 112 | } 113 | } 114 | 115 | class _ClipInsetShapeShadowPainter extends CustomPainter { 116 | final List? shadows; 117 | final CustomClipper clipper; 118 | 119 | _ClipInsetShapeShadowPainter({ 120 | required this.shadows, 121 | required this.clipper, 122 | }); 123 | 124 | @override 125 | void paint(Canvas canvas, Size size) { 126 | shadows?.forEach((element) { 127 | Rect rect = Rect.fromLTRB(0, 0, size.width, size.height) 128 | .deflate(element.spreadRadius) 129 | .expandToInclude(Rect.zero); 130 | 131 | Rect outerRect = Rect.fromLTRB(0, 0, size.width, size.height) 132 | .inflate(element.blurRadius); 133 | 134 | var paint = element.toPaint() 135 | ..shader = element.gradient?.createShader(rect); 136 | Path clipPath; 137 | if (rect.isEmpty) { 138 | clipPath = Path()..moveTo(rect.center.dx, rect.center.dy); 139 | } else { 140 | clipPath = 141 | clipper.getClip(rect.size).shift(element.offset + rect.topLeft); 142 | } 143 | Path outerPath = Path() 144 | ..moveTo(outerRect.topLeft.dx, outerRect.topLeft.dy) 145 | ..lineTo(outerRect.topRight.dx, outerRect.topRight.dy) 146 | ..lineTo(outerRect.bottomRight.dx, outerRect.bottomRight.dy) 147 | ..lineTo(outerRect.bottomLeft.dx, outerRect.bottomLeft.dy) 148 | ..close(); 149 | 150 | Path finalPath = Path.combine( 151 | PathOperation.difference, 152 | outerPath, 153 | clipPath, 154 | ); 155 | canvas.drawPath(finalPath, paint); 156 | }); 157 | } 158 | 159 | @override 160 | bool shouldRepaint(_ClipInsetShapeShadowPainter oldDelegate) { 161 | return oldDelegate.clipper != clipper || oldDelegate.shadows != shadows; 162 | } 163 | } 164 | 165 | class _ShapeBorderPaint extends StatelessWidget { 166 | const _ShapeBorderPaint({ 167 | this.child, 168 | this.shape, 169 | this.borderOnForeground = true, 170 | }); 171 | 172 | final Widget? child; 173 | final ShapeBorder? shape; 174 | final bool borderOnForeground; 175 | 176 | @override 177 | Widget build(BuildContext context) { 178 | return CustomPaint( 179 | child: child, 180 | painter: borderOnForeground 181 | ? null 182 | : _ShapeBorderPainter(shape, Directionality.maybeOf(context)), 183 | foregroundPainter: borderOnForeground 184 | ? _ShapeBorderPainter(shape, Directionality.maybeOf(context)) 185 | : null, 186 | ); 187 | } 188 | } 189 | 190 | class _ShapeBorderPainter extends CustomPainter { 191 | _ShapeBorderPainter(this.border, this.textDirection); 192 | final ShapeBorder? border; 193 | final TextDirection? textDirection; 194 | 195 | @override 196 | void paint(Canvas canvas, Size size) { 197 | border?.paint(canvas, Offset.zero & size, textDirection: textDirection); 198 | } 199 | 200 | @override 201 | bool shouldRepaint(_ShapeBorderPainter oldDelegate) { 202 | return oldDelegate.border != border; 203 | } 204 | } 205 | 206 | class DecoratedShadowedShape extends StatelessWidget { 207 | final ShapeBorder? shape; 208 | final List? shadows; 209 | final List? insetShadows; 210 | final Decoration? decoration; 211 | final Widget? child; 212 | 213 | DecoratedShadowedShape( 214 | {this.shape, 215 | this.shadows, 216 | this.insetShadows, 217 | this.decoration, 218 | this.child}); 219 | 220 | @override 221 | Widget build(BuildContext context) { 222 | CustomClipper clipper = CustomShapeBorderClipper( 223 | shape: shape ?? RectangleShapeBorder(), 224 | textDirection: Directionality.maybeOf(context), 225 | ); 226 | return _ShapeBorderPaint( 227 | shape: shape, 228 | child: ClipPathShadow( 229 | clipper: clipper, 230 | shadows: shadows, 231 | child: ClipPathInsetShadow( 232 | decoration: decoration, 233 | clipper: clipper, 234 | shadows: insetShadows, 235 | child: child), 236 | ), 237 | ); 238 | } 239 | } 240 | -------------------------------------------------------------------------------- /lib/src/dynamic_path/border_paths.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:morphable_shape/src/dynamic_path/dynamic_path.dart'; 3 | 4 | ///border paths for a FilledBorderShape 5 | ///has an outer dynamic path, an inner dynamic path and 6 | ///a list of fill colors/gradients. 7 | ///Returns a list of closed paths constructed 8 | ///from each pair of points on the outer and inner 9 | ///path and a fill color/gradient 10 | class BorderPaths { 11 | static double tolerancePercent = 0.001; 12 | 13 | DynamicPath outer; 14 | DynamicPath inner; 15 | List fillColors; 16 | List fillGradients; 17 | 18 | BorderPaths( 19 | {required this.outer, 20 | required this.inner, 21 | required this.fillColors, 22 | required this.fillGradients}); 23 | 24 | void removeOverlappingPaths() { 25 | assert(outer.nodes.length == inner.nodes.length); 26 | assert(outer.nodes.length == fillColors.length); 27 | assert(outer.nodes.length == fillGradients.length); 28 | 29 | if (outer.nodes.isNotEmpty) { 30 | double pointGroupWeight = 1; 31 | List outerNodes = [outer.nodes[0]]; 32 | List innerNodes = [inner.nodes[0]]; 33 | List newColors = [fillColors[0]]; 34 | List newGradients = [fillGradients[0]]; 35 | for (int i = 1; i < outer.nodes.length; i++) { 36 | if ((outer.nodes[i].position - outerNodes.last.position).distance < 37 | tolerancePercent * outer.size.shortestSide && 38 | (inner.nodes[i].position - innerNodes.last.position).distance < 39 | tolerancePercent * outer.size.shortestSide) { 40 | outerNodes.last.next = outer.nodes[i].next; 41 | outerNodes.last.position += 42 | (outer.nodes[i].position - outerNodes.last.position) / 43 | (pointGroupWeight + 1); 44 | innerNodes.last.next = inner.nodes[i].next; 45 | innerNodes.last.position += 46 | (inner.nodes[i].position - innerNodes.last.position) / 47 | (pointGroupWeight + 1); 48 | newColors.last = fillColors[i]; 49 | newGradients.last = fillGradients[i]; 50 | 51 | pointGroupWeight++; 52 | } else if (i == outer.nodes.length - 1 && 53 | (outer.nodes[i].position - outerNodes.first.position).distance < 54 | tolerancePercent * outer.size.shortestSide && 55 | (inner.nodes[i].position - innerNodes.first.position).distance < 56 | tolerancePercent * outer.size.shortestSide) { 57 | outerNodes.first.prev = outer.nodes[i].prev; 58 | outerNodes.first.position += 59 | (outer.nodes[i].position - outerNodes.first.position) / 60 | (pointGroupWeight + 1); 61 | innerNodes.first.prev = inner.nodes[i].prev; 62 | innerNodes.first.position += 63 | (inner.nodes[i].position - innerNodes.first.position) / 64 | (pointGroupWeight + 1); 65 | newColors.first = fillColors[i]; 66 | newGradients.first = fillGradients[i]; 67 | } else { 68 | pointGroupWeight = 1; 69 | outerNodes.add(outer.nodes[i]); 70 | innerNodes.add(inner.nodes[i]); 71 | newColors.add(fillColors[i]); 72 | newGradients.add(fillGradients[i]); 73 | } 74 | } 75 | outer.nodes = outerNodes; 76 | inner.nodes = innerNodes; 77 | 78 | fillColors = newColors; 79 | fillGradients = newGradients; 80 | } 81 | } 82 | 83 | List generateBorderPaths(Rect rect) { 84 | int pathLength = outer.nodes.length; 85 | List rst = []; 86 | 87 | for (int i = 0; i < pathLength; i++) { 88 | DynamicNode nextNode = outer.nodes[(i + 1) % pathLength]; 89 | DynamicPath borderPath = DynamicPath(size: rect.size, nodes: []); 90 | borderPath.nodes.add(DynamicNode( 91 | position: outer.nodes[i].position, next: outer.nodes[i].next)); 92 | borderPath.nodes 93 | .add(DynamicNode(position: nextNode.position, prev: nextNode.prev)); 94 | DynamicNode nextInnerNode = inner.nodes[(i + 1) % pathLength]; 95 | borderPath.nodes.add(DynamicNode( 96 | position: nextInnerNode.position, next: nextInnerNode.prev)); 97 | borderPath.nodes.add(DynamicNode( 98 | position: inner.nodes[i % pathLength].position, 99 | prev: inner.nodes[i % pathLength].next)); 100 | rst.add(borderPath.getPath(rect.size)); 101 | } 102 | 103 | return rst; 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /lib/src/morphable_shape_border.dart: -------------------------------------------------------------------------------- 1 | import 'dart:ui'; 2 | 3 | import 'package:morphable_shape/src/common_includes.dart'; 4 | import 'package:morphable_shape/src/dynamic_path/border_paths.dart'; 5 | 6 | ///The base class for various shape borders implemented in this package 7 | ///should be serializable/deserializable 8 | ///generate a DynamicPath instance with all the control points, then convert to a Path 9 | abstract class MorphableShapeBorder extends ShapeBorder { 10 | const MorphableShapeBorder(); 11 | 12 | Map toJson(); 13 | 14 | MorphableShapeBorder copyWith(); 15 | 16 | EdgeInsetsGeometry get dimensions; 17 | 18 | ///TODO: implement this, 19 | ///not a top priority as there is no use case I can think of... 20 | @override 21 | ShapeBorder scale(double t) { 22 | return this; 23 | } 24 | 25 | bool isSameMorphGeometry(MorphableShapeBorder shape); 26 | 27 | DynamicPath generateOuterDynamicPath(Rect rect); 28 | 29 | DynamicPath generateInnerDynamicPath(Rect rect); 30 | 31 | @override 32 | Path getOuterPath(Rect rect, {TextDirection? textDirection}) { 33 | DynamicPath path = generateOuterDynamicPath(Offset.zero & rect.size); 34 | path.removeOverlappingNodes(); 35 | return path.getPath(rect.size).shift(rect.topLeft); 36 | } 37 | 38 | @override 39 | Path getInnerPath(Rect rect, {TextDirection? textDirection}) { 40 | DynamicPath path = generateInnerDynamicPath(Offset.zero & rect.size); 41 | path.removeOverlappingNodes(); 42 | return path.getPath(rect.size).shift(rect.topLeft); 43 | } 44 | 45 | @override 46 | void paint(Canvas canvas, Rect rect, {TextDirection? textDirection}); 47 | } 48 | 49 | ///Shape with a single border color and width, use PaintingStyle.stroke 50 | ///to paint the border 51 | abstract class OutlinedShapeBorder extends MorphableShapeBorder { 52 | final DynamicBorderSide border; 53 | 54 | const OutlinedShapeBorder({this.border = DynamicBorderSide.none}); 55 | 56 | EdgeInsetsGeometry get dimensions => EdgeInsets.all(border.width); 57 | 58 | Map toJson() { 59 | return {"border": border.toJson()}; 60 | } 61 | 62 | OutlinedShapeBorder copyWith({ 63 | DynamicBorderSide? border, 64 | }) { 65 | return this.copyWith(border: border); 66 | } 67 | 68 | DynamicPath generateInnerDynamicPath(Rect rect) { 69 | return generateOuterDynamicPath(rect); 70 | } 71 | 72 | @override 73 | void paint(Canvas canvas, Rect rect, {TextDirection? textDirection}) { 74 | Paint borderPaint = Paint(); 75 | 76 | if (border.style != BorderStyle.none) { 77 | borderPaint.isAntiAlias = true; 78 | borderPaint.style = PaintingStyle.stroke; 79 | borderPaint.color = border.color; 80 | borderPaint.strokeWidth = border.width; 81 | borderPaint.shader = border.gradient?.createShader(rect); 82 | borderPaint.strokeCap = border.strokeCap; 83 | borderPaint.strokeJoin = border.strokeJoin; 84 | drawBorderPath(canvas, rect, borderPaint, getOuterPath(rect), border); 85 | } 86 | } 87 | 88 | ///draw the border with a optional begin, end, and offset 89 | static void drawBorderPath(Canvas canvas, Rect rect, Paint borderPaint, 90 | Path path, DynamicBorderSide border) { 91 | if (border.begin != null || border.end != null) { 92 | PathMetric metric = path.computeMetrics().first; 93 | 94 | double beginPX = border.begin?.toPX(constraint: metric.length) ?? 0.0; 95 | double endPX = 96 | border.end?.toPX(constraint: metric.length) ?? metric.length; 97 | double shiftPX = border.shift?.toPX(constraint: metric.length) ?? 0.0; 98 | double temp = beginPX; 99 | beginPX = beginPX > endPX ? endPX : beginPX; 100 | endPX = beginPX == endPX ? temp : endPX; 101 | beginPX = beginPX.clamp(0, metric.length); 102 | endPX = endPX.clamp(0, metric.length); 103 | shiftPX = shiftPX.clamp(0, metric.length); 104 | 105 | path = extractPartialPath( 106 | metric, 107 | beginPX + shiftPX, 108 | endPX + shiftPX, 109 | ); 110 | if (beginPX + shiftPX < metric.length) { 111 | canvas.drawPath(path, borderPaint); 112 | if (endPX + shiftPX > metric.length) { 113 | path = extractPartialPath( 114 | metric, 115 | 0.0, 116 | endPX + shiftPX - metric.length, 117 | ); 118 | canvas.drawPath(path, borderPaint); 119 | } 120 | } else { 121 | path = extractPartialPath( 122 | metric, 123 | beginPX + shiftPX - metric.length, 124 | endPX + shiftPX - metric.length, 125 | ); 126 | canvas.drawPath(path, borderPaint); 127 | } 128 | } else { 129 | canvas.drawPath(path, borderPaint); 130 | } 131 | } 132 | 133 | static Path extractPartialPath(PathMetric metric, double begin, double end) { 134 | if (begin <= 0.0 && end >= metric.length) { 135 | return metric.extractPath( 136 | begin, 137 | end, 138 | )..close(); 139 | } 140 | return metric.extractPath( 141 | begin, 142 | end, 143 | ); 144 | } 145 | } 146 | 147 | ///Shape with multiple border color and width, use PaintingStyle.fill 148 | ///to paint the border 149 | abstract class FilledBorderShapeBorder extends MorphableShapeBorder { 150 | const FilledBorderShapeBorder(); 151 | 152 | List borderFillColors(); 153 | 154 | List borderFillGradients(); 155 | 156 | @override 157 | void paint(Canvas canvas, Rect rect, {TextDirection? textDirection}) { 158 | Paint borderPaint = Paint(); 159 | 160 | DynamicPath outer = generateOuterDynamicPath(rect); 161 | DynamicPath inner = generateInnerDynamicPath(rect); 162 | List borderColors = borderFillColors(); 163 | List borderGradients = borderFillGradients(); 164 | 165 | BorderPaths borderPaths = BorderPaths( 166 | outer: outer, 167 | inner: inner, 168 | fillColors: borderColors, 169 | fillGradients: borderGradients); 170 | 171 | borderPaths.removeOverlappingPaths(); 172 | 173 | List paths = borderPaths.generateBorderPaths(rect); 174 | borderColors = borderPaths.fillColors; 175 | borderGradients = borderPaths.fillGradients; 176 | List finalPaths = [paths[0]]; 177 | List finalColors = [borderColors[0]]; 178 | List finalGradients = [borderGradients[0]]; 179 | for (int i = 1; i < paths.length; i++) { 180 | if (i < paths.length - 1 && 181 | borderGradients[i] == borderGradients[i - 1] && 182 | borderColors[i] == borderColors[i - 1]) { 183 | finalPaths.last = 184 | Path.combine(PathOperation.union, finalPaths.last, paths[i]); 185 | } else { 186 | finalPaths.add(paths[i]); 187 | finalColors.add(borderColors[i]); 188 | finalGradients.add(borderGradients[i]); 189 | } 190 | } 191 | for (int i = 0; i < finalPaths.length; i++) { 192 | borderPaint.isAntiAlias = true; 193 | borderPaint.style = PaintingStyle.fill; 194 | borderPaint.color = finalColors[i]; 195 | borderPaint.shader = finalGradients[i]?.createShader(rect); 196 | borderPaint.strokeWidth = 1; 197 | borderPaint.strokeMiterLimit = 0.0; 198 | canvas.drawPath(finalPaths[i], borderPaint); 199 | } 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /lib/src/morphable_shape_border_tween.dart: -------------------------------------------------------------------------------- 1 | import 'package:morphable_shape/src/common_includes.dart'; 2 | import 'package:morphable_shape/src/shape_borders/morph.dart'; 3 | 4 | ///Why is there no shapeTween? 5 | ///Because to morph shape we need to know the rect at every time step, 6 | /// which can only be retrieved from a shapeBorder 7 | class MorphableShapeBorderTween extends Tween { 8 | MorphShapeData? data; 9 | MorphMethod method; 10 | MorphableShapeBorderTween( 11 | {MorphableShapeBorder? begin, 12 | MorphableShapeBorder? end, 13 | this.method = MorphMethod.auto}) 14 | : super(begin: begin, end: end); 15 | 16 | @override 17 | MorphableShapeBorder? lerp(double t) { 18 | if (begin == null && end == null) { 19 | return null; 20 | } 21 | if (begin == null) { 22 | if (data == null || end != data!.end) { 23 | data = MorphShapeData( 24 | begin: RectangleShapeBorder(), 25 | end: !(end! is MorphShapeBorder) 26 | ? end! 27 | : (end! as MorphShapeBorder).morphData.end, 28 | boundingBox: Rect.fromLTRB(0, 0, 100, 100), 29 | method: method); 30 | DynamicPathMorph.sampleBorderPathsFromShape(data!); 31 | } 32 | return MorphShapeBorder(t: t, morphData: data!); 33 | } else if (end == null) { 34 | if (data == null) { 35 | data = MorphShapeData( 36 | begin: !(begin! is MorphShapeBorder) 37 | ? begin! 38 | : (begin! as MorphShapeBorder).morphData.begin, 39 | end: RectangleShapeBorder(), 40 | boundingBox: Rect.fromLTRB(0, 0, 100, 100), 41 | method: method); 42 | DynamicPathMorph.sampleBorderPathsFromShape(data!); 43 | } 44 | return MorphShapeBorder(t: t, morphData: data!); 45 | } 46 | 47 | if (data == null || begin != data!.begin || end != data!.end) { 48 | MorphableShapeBorder beginShape = !(begin! is MorphShapeBorder) 49 | ? begin! 50 | : (begin! as MorphShapeBorder).morphData.begin; 51 | MorphableShapeBorder endShape = !(end! is MorphShapeBorder) 52 | ? end! 53 | : (end! as MorphShapeBorder).morphData.end; 54 | data = MorphShapeData( 55 | begin: beginShape, 56 | end: endShape, 57 | boundingBox: Rect.fromLTRB(0, 0, 100, 100), 58 | method: method); 59 | DynamicPathMorph.sampleBorderPathsFromShape(data!); 60 | } 61 | return MorphShapeBorder(t: t, morphData: data!); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /lib/src/parse_json.dart: -------------------------------------------------------------------------------- 1 | import 'package:morphable_shape/src/common_includes.dart'; 2 | 3 | CornerStyle? parseCornerStyle(String? string) { 4 | if (string == null) return null; 5 | switch (string) { 6 | case "rounded": 7 | return CornerStyle.rounded; 8 | case "concave": 9 | return CornerStyle.concave; 10 | case "straight": 11 | return CornerStyle.straight; 12 | case "cutout": 13 | return CornerStyle.cutout; 14 | } 15 | return null; 16 | } 17 | 18 | ShapeSide? parseShapeSide(String? string) { 19 | if (string == null) return null; 20 | switch (string) { 21 | case "top": 22 | return ShapeSide.top; 23 | case "bottom": 24 | return ShapeSide.bottom; 25 | case "left": 26 | return ShapeSide.left; 27 | case "right": 28 | return ShapeSide.right; 29 | } 30 | return null; 31 | } 32 | 33 | DynamicBorderSide? parseDynamicBorderSide(Map? map) { 34 | if (map == null) return null; 35 | return DynamicBorderSide.fromJson(map); 36 | } 37 | 38 | RectangleBorderSides? parseRectangleBorderSide(Map? map) { 39 | if (map == null) return null; 40 | return RectangleBorderSides.fromJson(map); 41 | } 42 | 43 | RectangleCornerStyles? parseRectangleCornerStyle(Map? map) { 44 | if (map == null) return null; 45 | return RectangleCornerStyles.fromJson(map); 46 | } 47 | 48 | DynamicRadius? parseDynamicRadius(Map? map) { 49 | if (map == null) return null; 50 | return DynamicRadius.fromJson(map); 51 | } 52 | 53 | DynamicBorderRadius? parseDynamicBorderRadius(Map? map) { 54 | if (map == null) return null; 55 | return DynamicBorderRadius.fromJson(map); 56 | } 57 | 58 | DynamicOffset? parseDynamicOffset(Map? map) { 59 | if (map == null) return null; 60 | Dimension dx = parseDimension(map['dx']) ?? Length(0); 61 | Dimension dy = parseDimension(map['dy']) ?? Length(0); 62 | return DynamicOffset(dx, dy); 63 | } 64 | 65 | DynamicPath? parseDynamicPath(Map? map) { 66 | if (map == null) return null; 67 | Size? size = parseSize(map["size"]); 68 | List? nodes = 69 | (map["nodes"] as List?)?.map((e) => DynamicNode.fromJson(e)).toList(); 70 | if (size == null || nodes == null) { 71 | return null; 72 | } else { 73 | return DynamicPath(size: size, nodes: nodes); 74 | } 75 | } 76 | 77 | ShapeShadow? parseShapeShadow(Map? map) { 78 | if (map == null) return null; 79 | Color color = parseColor(map["color"]) ?? Colors.transparent; 80 | Offset offset = parseOffset(map["offset"]) ?? Offset.zero; 81 | double blurRadius = (map["blurRadius"] ?? 0.0).toDouble(); 82 | double spreadRadius = (map["spreadRadius"] ?? 0.0).toDouble(); 83 | Gradient? gradient = parseGradient(map["gradient"]); 84 | 85 | return ShapeShadow( 86 | color: color, 87 | offset: offset, 88 | blurRadius: blurRadius, 89 | spreadRadius: spreadRadius, 90 | gradient: gradient); 91 | } 92 | 93 | MorphableShapeBorder? parseMorphableShapeBorder(Map? map) { 94 | if (map == null || map["type"] == null) return null; 95 | 96 | String shapeName = map["type"]; 97 | switch (shapeName) { 98 | case "Arc": 99 | return ArcShapeBorder.fromJson(map); 100 | case "Arrow": 101 | return ArrowShapeBorder.fromJson(map); 102 | case "Bubble": 103 | return BubbleShapeBorder.fromJson(map); 104 | case "Circle": 105 | return CircleShapeBorder.fromJson(map); 106 | case "Polygon": 107 | return PolygonShapeBorder.fromJson(map); 108 | case "Rectangle": 109 | return RectangleShapeBorder.fromJson(map); 110 | case "RoundedRectangle": 111 | return RoundedRectangleShapeBorder.fromJson(map); 112 | case "Star": 113 | return StarShapeBorder.fromJson(map); 114 | case "Trapezoid": 115 | return TrapezoidShapeBorder.fromJson(map); 116 | case "Triangle": 117 | return TriangleShapeBorder.fromJson(map); 118 | case "Path": 119 | return PathShapeBorder.fromJson(map); 120 | default: 121 | return null; 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /lib/src/shape_borders/arc.dart: -------------------------------------------------------------------------------- 1 | import 'package:morphable_shape/src/common_includes.dart'; 2 | 3 | ///A rectangle with one side replaced by an arc with a certain height 4 | class ArcShapeBorder extends OutlinedShapeBorder { 5 | final ShapeSide side; 6 | final Dimension arcHeight; 7 | final bool isOutward; 8 | 9 | const ArcShapeBorder({ 10 | DynamicBorderSide border = DynamicBorderSide.none, 11 | this.side = ShapeSide.bottom, 12 | this.isOutward = true, 13 | this.arcHeight = const Length(20), 14 | }) : super(border: border); 15 | 16 | ArcShapeBorder.fromJson(Map map) 17 | : side = parseShapeSide(map['side']) ?? ShapeSide.bottom, 18 | isOutward = map["isOutward"], 19 | arcHeight = parseDimension(map["arcHeight"]) ?? Length(20), 20 | super( 21 | border: parseDynamicBorderSide(map["border"]) ?? 22 | DynamicBorderSide.none); 23 | 24 | Map toJson() { 25 | Map rst = {"type": "Arc"}; 26 | rst.addAll(super.toJson()); 27 | rst["arcHeight"] = arcHeight.toJson(); 28 | rst["isOutward"] = isOutward; 29 | rst["side"] = side.toJson(); 30 | 31 | return rst; 32 | } 33 | 34 | ArcShapeBorder copyWith({ 35 | ShapeSide? side, 36 | bool? isOutward, 37 | Dimension? arcHeight, 38 | DynamicBorderSide? border, 39 | }) { 40 | return ArcShapeBorder( 41 | side: side ?? this.side, 42 | isOutward: isOutward ?? this.isOutward, 43 | arcHeight: arcHeight ?? this.arcHeight, 44 | border: border ?? this.border, 45 | ); 46 | } 47 | 48 | bool isSameMorphGeometry(MorphableShapeBorder shape) { 49 | return shape is ArcShapeBorder && shape.side == this.side; 50 | } 51 | 52 | DynamicPath generateOuterDynamicPath(Rect rect) { 53 | final size = rect.size; 54 | 55 | double maximumSize = min(size.height, size.height) / 2; 56 | 57 | double arcHeight = 0; 58 | if (this.side.isHorizontal) { 59 | arcHeight = 60 | this.arcHeight.toPX(constraint: size.height).clamp(0, maximumSize); 61 | } else { 62 | arcHeight = 63 | this.arcHeight.toPX(constraint: size.width).clamp(0, maximumSize); 64 | } 65 | double theta1, theta2, theta3, radius; 66 | if (this.side.isHorizontal) { 67 | theta1 = atan(size.width / (2 * arcHeight)); 68 | theta2 = atan((2 * arcHeight) / size.width); 69 | theta3 = theta1 - theta2; 70 | radius = size.width / 2 * tan(theta3) + arcHeight; 71 | } else { 72 | theta1 = atan(size.height / (2 * arcHeight)); 73 | theta2 = atan((2 * arcHeight) / size.height); 74 | theta3 = theta1 - theta2; 75 | radius = size.height / 2 * tan(theta3) + arcHeight; 76 | } 77 | 78 | List nodes = []; 79 | 80 | if (arcHeight == 0) { 81 | nodes.add(DynamicNode(position: Offset(0, 0.0))); 82 | nodes.add(DynamicNode(position: Offset(size.width, 0.0))); 83 | nodes.add(DynamicNode(position: Offset(size.width, size.height))); 84 | nodes.add(DynamicNode(position: Offset(0, size.height))); 85 | return DynamicPath(nodes: nodes, size: size); 86 | } 87 | 88 | switch (this.side) { 89 | case ShapeSide.top: 90 | if (isOutward) { 91 | Rect circleRect = Rect.fromCenter( 92 | center: Offset(size.width / 2, radius), 93 | width: 2 * radius, 94 | height: 2 * radius); 95 | double startAngle = pi + theta3; 96 | double sweepAngle = pi - 2 * theta3; 97 | 98 | nodes.addArc(circleRect, startAngle, sweepAngle); 99 | nodes.add(DynamicNode(position: Offset(size.width, size.height))); 100 | nodes.add(DynamicNode(position: Offset(0.0, size.height))); 101 | } else { 102 | Rect circleRect = Rect.fromCenter( 103 | center: Offset(size.width / 2, arcHeight - radius), 104 | width: 2 * radius, 105 | height: 2 * radius); 106 | double startAngle = pi - theta3; 107 | double sweepAngle = pi - 2 * theta3; 108 | 109 | nodes.addArc(circleRect, startAngle, -sweepAngle); 110 | nodes.add(DynamicNode(position: Offset(size.width, size.height))); 111 | nodes.add(DynamicNode(position: Offset(0.0, size.height))); 112 | } 113 | break; 114 | case ShapeSide.bottom: 115 | if (isOutward) { 116 | Rect circleRect = Rect.fromCenter( 117 | center: Offset(size.width / 2, size.height - radius), 118 | width: 2 * radius, 119 | height: 2 * radius); 120 | double startAngle = theta3; 121 | double sweepAngle = pi - 2 * theta3; 122 | 123 | nodes.add(DynamicNode(position: Offset(0.0, 0.0))); 124 | nodes.add(DynamicNode(position: Offset(size.width, 0.0))); 125 | nodes.addArc(circleRect, startAngle, sweepAngle); 126 | } else { 127 | Rect circleRect = Rect.fromCenter( 128 | center: Offset(size.width / 2, size.height - arcHeight + radius), 129 | width: 2 * radius, 130 | height: 2 * radius); 131 | double startAngle = -theta3; 132 | double sweepAngle = pi - 2 * theta3; 133 | nodes.add(DynamicNode(position: Offset(0.0, 0.0))); 134 | nodes.add(DynamicNode(position: Offset(size.width, 0.0))); 135 | nodes.addArc(circleRect, startAngle, -sweepAngle); 136 | } 137 | break; 138 | case ShapeSide.left: 139 | if (isOutward) { 140 | Rect circleRect = Rect.fromCenter( 141 | center: Offset(radius, size.height / 2), 142 | width: 2 * radius, 143 | height: 2 * radius); 144 | double startAngle = pi / 2 + theta3; 145 | double sweepAngle = pi - 2 * theta3; 146 | nodes.add(DynamicNode(position: Offset(arcHeight, 0.0))); 147 | nodes.add(DynamicNode(position: Offset(size.width, 0.0))); 148 | nodes.add(DynamicNode(position: Offset(size.width, size.height))); 149 | nodes.addArc(circleRect, startAngle, sweepAngle); 150 | } else { 151 | Rect circleRect = Rect.fromCenter( 152 | center: Offset(arcHeight - radius, size.height / 2), 153 | width: 2 * radius, 154 | height: 2 * radius); 155 | double startAngle = pi / 2 - theta3; 156 | double sweepAngle = pi - 2 * theta3; 157 | nodes.add(DynamicNode(position: Offset(0.0, 0.0))); 158 | nodes.add(DynamicNode(position: Offset(size.width, 0.0))); 159 | nodes.add(DynamicNode(position: Offset(size.width, size.height))); 160 | nodes.addArc(circleRect, startAngle, -sweepAngle); 161 | } 162 | break; 163 | case ShapeSide.right: //right 164 | if (isOutward) { 165 | Rect circleRect = Rect.fromCenter( 166 | center: Offset(size.width - radius, size.height / 2), 167 | width: 2 * radius, 168 | height: 2 * radius); 169 | double startAngle = -(pi / 2 - theta3); 170 | double sweepAngle = pi - 2 * theta3; 171 | nodes.addArc(circleRect, startAngle, sweepAngle); 172 | nodes.add(DynamicNode(position: Offset(0.0, size.height))); 173 | nodes.add(DynamicNode(position: Offset(0.0, 0.0))); 174 | } else { 175 | Rect circleRect = Rect.fromCenter( 176 | center: Offset(size.width - arcHeight + radius, size.height / 2), 177 | width: 2 * radius, 178 | height: 2 * radius); 179 | double startAngle = -(pi / 2 + theta3); 180 | double sweepAngle = pi - 2 * theta3; 181 | nodes.addArc(circleRect, startAngle, -sweepAngle); 182 | nodes.add(DynamicNode(position: Offset(0.0, size.height))); 183 | nodes.add(DynamicNode(position: Offset(0.0, 0.0))); 184 | } 185 | break; 186 | } 187 | return DynamicPath(nodes: nodes, size: size); 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /lib/src/shape_borders/arrow.dart: -------------------------------------------------------------------------------- 1 | import 'package:morphable_shape/src/common_includes.dart'; 2 | 3 | ///An arrow shape with a head and a tail 4 | class ArrowShapeBorder extends OutlinedShapeBorder { 5 | final ShapeSide side; 6 | final Dimension arrowHeight; 7 | final Dimension tailWidth; 8 | 9 | const ArrowShapeBorder( 10 | {this.side = ShapeSide.right, 11 | this.arrowHeight = const Length(25, unit: LengthUnit.percent), 12 | this.tailWidth = const Length(40, unit: LengthUnit.percent), 13 | DynamicBorderSide border = DynamicBorderSide.none}) 14 | : super(border: border); 15 | 16 | ArrowShapeBorder.fromJson(Map map) 17 | : side = parseShapeSide(map["side"]) ?? ShapeSide.bottom, 18 | arrowHeight = parseDimension(map['arrowHeight']) ?? 19 | Length(25, unit: LengthUnit.percent), 20 | tailWidth = parseDimension(map['tailWidth']) ?? 21 | Length(40, unit: LengthUnit.percent), 22 | super( 23 | border: parseDynamicBorderSide(map["border"]) ?? 24 | DynamicBorderSide.none); 25 | 26 | Map toJson() { 27 | Map rst = {"type": "Arrow"}; 28 | rst.addAll(super.toJson()); 29 | rst["side"] = side.toJson(); 30 | rst["arrowHeight"] = arrowHeight.toJson(); 31 | rst["tailWidth"] = tailWidth.toJson(); 32 | return rst; 33 | } 34 | 35 | ArrowShapeBorder copyWith({ 36 | ShapeSide? side, 37 | Dimension? arrowHeight, 38 | Dimension? tailWidth, 39 | DynamicBorderSide? border, 40 | }) { 41 | return ArrowShapeBorder( 42 | side: side ?? this.side, 43 | arrowHeight: arrowHeight ?? this.arrowHeight, 44 | tailWidth: tailWidth ?? this.tailWidth, 45 | border: border ?? this.border); 46 | } 47 | 48 | bool isSameMorphGeometry(MorphableShapeBorder shape) { 49 | return shape is ArrowShapeBorder && shape.side == this.side; 50 | } 51 | 52 | DynamicPath generateOuterDynamicPath(Rect rect) { 53 | List nodes = []; 54 | 55 | Size size = rect.size; 56 | 57 | double tailWidth, arrowHeight; 58 | if (side.isHorizontal) { 59 | arrowHeight = 60 | this.arrowHeight.toPX(constraint: size.height).clamp(0, size.height); 61 | tailWidth = 62 | this.tailWidth.toPX(constraint: size.width).clamp(0, size.width); 63 | } else { 64 | arrowHeight = 65 | this.arrowHeight.toPX(constraint: size.width).clamp(0, size.width); 66 | tailWidth = 67 | this.tailWidth.toPX(constraint: size.height).clamp(0, size.height); 68 | } 69 | 70 | switch (side) { 71 | case ShapeSide.top: 72 | { 73 | nodes.add(DynamicNode(position: Offset(size.width / 2, 0))); 74 | nodes.add(DynamicNode(position: Offset(size.width, arrowHeight))); 75 | nodes.add(DynamicNode( 76 | position: Offset(size.width / 2 + tailWidth / 2, arrowHeight))); 77 | nodes.add(DynamicNode( 78 | position: Offset(size.width / 2 + tailWidth / 2, size.height))); 79 | nodes.add(DynamicNode( 80 | position: Offset(size.width / 2 - tailWidth / 2, size.height))); 81 | nodes.add(DynamicNode( 82 | position: Offset(size.width / 2 - tailWidth / 2, arrowHeight))); 83 | nodes.add(DynamicNode(position: Offset(0, arrowHeight))); 84 | } 85 | break; 86 | case ShapeSide.bottom: 87 | { 88 | nodes.add(DynamicNode(position: Offset(size.width / 2, size.height))); 89 | nodes 90 | .add(DynamicNode(position: Offset(0, size.height - arrowHeight))); 91 | nodes.add(DynamicNode( 92 | position: Offset( 93 | size.width / 2 - tailWidth / 2, size.height - arrowHeight))); 94 | nodes.add( 95 | DynamicNode(position: Offset(size.width / 2 - tailWidth / 2, 0))); 96 | nodes.add( 97 | DynamicNode(position: Offset(size.width / 2 + tailWidth / 2, 0))); 98 | nodes.add(DynamicNode( 99 | position: Offset( 100 | size.width / 2 + tailWidth / 2, size.height - arrowHeight))); 101 | nodes.add(DynamicNode( 102 | position: Offset(size.width, size.height - arrowHeight))); 103 | } 104 | break; 105 | case ShapeSide.left: 106 | { 107 | nodes.add(DynamicNode(position: Offset(0, size.height / 2))); 108 | nodes.add(DynamicNode(position: Offset(arrowHeight, 0))); 109 | nodes.add(DynamicNode( 110 | position: Offset(arrowHeight, size.height / 2 - tailWidth / 2))); 111 | nodes.add(DynamicNode( 112 | position: Offset(size.width, size.height / 2 - tailWidth / 2))); 113 | nodes.add(DynamicNode( 114 | position: Offset(size.width, size.height / 2 + tailWidth / 2))); 115 | nodes.add(DynamicNode( 116 | position: Offset(arrowHeight, size.height / 2 + tailWidth / 2))); 117 | nodes.add(DynamicNode(position: Offset(arrowHeight, size.height))); 118 | } 119 | break; 120 | case ShapeSide.right: 121 | { 122 | nodes.add(DynamicNode(position: Offset(size.width, size.height / 2))); 123 | nodes.add(DynamicNode( 124 | position: Offset(size.width - arrowHeight, size.height))); 125 | nodes.add(DynamicNode( 126 | position: Offset( 127 | size.width - arrowHeight, size.height / 2 + tailWidth / 2))); 128 | nodes.add(DynamicNode( 129 | position: Offset(0, size.height / 2 + tailWidth / 2))); 130 | nodes.add(DynamicNode( 131 | position: Offset(0, size.height / 2 - tailWidth / 2))); 132 | nodes.add(DynamicNode( 133 | position: Offset( 134 | size.width - arrowHeight, size.height / 2 - tailWidth / 2))); 135 | nodes.add(DynamicNode(position: Offset(size.width - arrowHeight, 0))); 136 | } 137 | 138 | break; 139 | } 140 | 141 | return DynamicPath(size: rect.size, nodes: nodes); 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /lib/src/shape_borders/bubble.dart: -------------------------------------------------------------------------------- 1 | import 'package:morphable_shape/src/common_includes.dart'; 2 | 3 | ///Bubble shape, with a triangular tip and equal radius rounded corner 4 | ///The corner parameter is where the tip calculates its positions 5 | class BubbleShapeBorder extends OutlinedShapeBorder { 6 | final ShapeSide side; 7 | 8 | final Dimension borderRadius; 9 | final Dimension arrowHeight; 10 | final Dimension arrowWidth; 11 | 12 | ///arrow position is calculated from the left (if at top or bottom) 13 | ///or from the top (if at left or right) 14 | ///if you want to calculate from the other side, you can use for example 15 | ///100.toPercentLength-10.toPXLength 16 | final Dimension arrowCenterPosition; 17 | final Dimension arrowHeadPosition; 18 | 19 | const BubbleShapeBorder({ 20 | DynamicBorderSide border = DynamicBorderSide.none, 21 | this.side = ShapeSide.bottom, 22 | this.borderRadius = const Length(6), 23 | this.arrowHeight = const Length(20, unit: LengthUnit.percent), 24 | this.arrowWidth = const Length(30, unit: LengthUnit.percent), 25 | this.arrowCenterPosition = const Length(50, unit: LengthUnit.percent), 26 | this.arrowHeadPosition = const Length(50, unit: LengthUnit.percent), 27 | }) : super(border: border); 28 | 29 | BubbleShapeBorder.fromJson(Map map) 30 | : side = parseShapeSide(map["side"]) ?? ShapeSide.bottom, 31 | borderRadius = parseDimension(map["borderRadius"]) ?? Length(6), 32 | arrowHeight = 33 | parseDimension(map["arrowHeight"]) ?? 20.0.toPercentLength, 34 | arrowWidth = parseDimension(map["arrowWidth"]) ?? 30.0.toPercentLength, 35 | arrowCenterPosition = 36 | parseDimension(map["arrowCenterPosition"]) ?? 50.0.toPercentLength, 37 | arrowHeadPosition = 38 | parseDimension(map["arrowHeadPosition"]) ?? 50.0.toPercentLength, 39 | super( 40 | border: parseDynamicBorderSide(map["border"]) ?? 41 | DynamicBorderSide.none); 42 | 43 | Map toJson() { 44 | Map rst = {"type": "Bubble"}; 45 | rst.addAll(super.toJson()); 46 | rst["side"] = side.toJson(); 47 | rst["borderRadius"] = borderRadius.toJson(); 48 | rst["arrowHeight"] = arrowHeight.toJson(); 49 | rst["arrowWidth"] = arrowWidth.toJson(); 50 | rst["arrowCenterPosition"] = arrowCenterPosition.toJson(); 51 | rst["arrowHeadPosition"] = arrowHeadPosition.toJson(); 52 | 53 | return rst; 54 | } 55 | 56 | BubbleShapeBorder copyWith({ 57 | ShapeSide? side, 58 | Dimension? borderRadius, 59 | Dimension? arrowHeight, 60 | Dimension? arrowWidth, 61 | Dimension? arrowCenterPosition, 62 | Dimension? arrowHeadPosition, 63 | DynamicBorderSide? border, 64 | }) { 65 | return BubbleShapeBorder( 66 | border: border ?? this.border, 67 | side: side ?? this.side, 68 | borderRadius: borderRadius ?? this.borderRadius, 69 | arrowHeight: arrowHeight ?? this.arrowHeight, 70 | arrowWidth: arrowWidth ?? this.arrowWidth, 71 | arrowCenterPosition: arrowCenterPosition ?? this.arrowCenterPosition, 72 | arrowHeadPosition: arrowHeadPosition ?? this.arrowHeadPosition, 73 | ); 74 | } 75 | 76 | bool isSameMorphGeometry(MorphableShapeBorder shape) { 77 | return shape is BubbleShapeBorder && this.side == shape.side; 78 | } 79 | 80 | DynamicPath generateOuterDynamicPath(Rect rect) { 81 | final size = rect.size; 82 | 83 | double borderRadius; 84 | double arrowHeight; 85 | double arrowWidth; 86 | double arrowCenterPosition; 87 | double arrowHeadPosition; 88 | borderRadius = 89 | this.borderRadius.toPX(constraint: min(size.height, size.width)); 90 | if (side.isHorizontal) { 91 | arrowHeight = this.arrowHeight.toPX(constraint: size.height); 92 | arrowWidth = this.arrowWidth.toPX(constraint: size.width); 93 | arrowCenterPosition = 94 | this.arrowCenterPosition.toPX(constraint: size.width); 95 | arrowHeadPosition = this.arrowHeadPosition.toPX(constraint: size.width); 96 | } else { 97 | arrowHeight = this.arrowHeight.toPX(constraint: size.width); 98 | arrowWidth = this.arrowWidth.toPX(constraint: size.height); 99 | arrowCenterPosition = 100 | this.arrowCenterPosition.toPX(constraint: size.height); 101 | arrowHeadPosition = this.arrowHeadPosition.toPX(constraint: size.height); 102 | } 103 | 104 | List nodes = []; 105 | 106 | final double spacingLeft = this.side == ShapeSide.left ? arrowHeight : 0; 107 | final double spacingTop = this.side == ShapeSide.top ? arrowHeight : 0; 108 | final double spacingRight = this.side == ShapeSide.right ? arrowHeight : 0; 109 | final double spacingBottom = 110 | this.side == ShapeSide.bottom ? arrowHeight : 0; 111 | 112 | final double left = spacingLeft + rect.left; 113 | final double top = spacingTop + rect.top; 114 | final double right = rect.right - spacingRight; 115 | final double bottom = rect.bottom - spacingBottom; 116 | 117 | double radiusBound = 0; 118 | 119 | if (this.side.isHorizontal) { 120 | arrowCenterPosition = arrowCenterPosition.clamp(0, size.width); 121 | arrowHeadPosition = arrowHeadPosition.clamp(0, size.width); 122 | arrowWidth = arrowWidth.clamp( 123 | 0, 2 * min(arrowCenterPosition, size.width - arrowCenterPosition)); 124 | radiusBound = min( 125 | min(right - arrowCenterPosition - arrowWidth / 2, 126 | arrowCenterPosition - arrowWidth / 2 - left), 127 | (bottom - top) / 2); 128 | borderRadius = 129 | borderRadius.clamp(0.0, radiusBound >= 0 ? radiusBound : 0); 130 | } else { 131 | arrowCenterPosition = arrowCenterPosition.clamp(0, size.height); 132 | arrowHeadPosition = arrowHeadPosition.clamp(0, size.height); 133 | arrowWidth = arrowWidth.clamp( 134 | 0, 2 * min(arrowCenterPosition, size.height - arrowCenterPosition)); 135 | radiusBound = min( 136 | min(bottom - arrowCenterPosition - arrowWidth / 2, 137 | arrowCenterPosition - arrowWidth / 2 - top), 138 | (right - left) / 2); 139 | borderRadius = borderRadius.clamp( 140 | 0.0, 141 | radiusBound >= 0 ? radiusBound : 0, 142 | ); 143 | } 144 | 145 | if (this.side == ShapeSide.top) { 146 | nodes.add(DynamicNode( 147 | position: Offset(arrowCenterPosition - arrowWidth / 2, top))); 148 | nodes.add(DynamicNode(position: Offset(arrowHeadPosition, rect.top))); 149 | nodes.add(DynamicNode( 150 | position: Offset(arrowCenterPosition + arrowWidth / 2, top))); 151 | } 152 | nodes.addArc( 153 | Rect.fromLTRB( 154 | right - 2 * borderRadius, top, right, top + 2 * borderRadius), 155 | 3 * pi / 2, 156 | pi / 2); 157 | 158 | if (this.side == ShapeSide.right) { 159 | nodes.add(DynamicNode( 160 | position: Offset(right, arrowCenterPosition - arrowWidth / 2))); 161 | nodes.add(DynamicNode(position: Offset(rect.right, arrowHeadPosition))); 162 | nodes.add(DynamicNode( 163 | position: Offset(right, arrowCenterPosition + arrowWidth / 2))); 164 | } 165 | nodes.addArc( 166 | Rect.fromLTRB( 167 | right - borderRadius * 2, bottom - borderRadius * 2, right, bottom), 168 | 0, 169 | pi / 2); 170 | 171 | if (this.side == ShapeSide.bottom) { 172 | nodes.add(DynamicNode( 173 | position: Offset(arrowCenterPosition + arrowWidth / 2, bottom))); 174 | nodes.add(DynamicNode(position: Offset(arrowHeadPosition, rect.bottom))); 175 | nodes.add(DynamicNode( 176 | position: Offset(arrowCenterPosition - arrowWidth / 2, bottom))); 177 | } 178 | nodes.addArc( 179 | Rect.fromLTRB( 180 | left, bottom - borderRadius * 2, left + borderRadius * 2, bottom), 181 | pi / 2, 182 | pi / 2); 183 | 184 | if (this.side == ShapeSide.left) { 185 | nodes.add(DynamicNode( 186 | position: Offset(left, arrowCenterPosition + arrowWidth / 2))); 187 | nodes.add(DynamicNode(position: Offset(rect.left, arrowHeadPosition))); 188 | nodes.add(DynamicNode( 189 | position: Offset(left, arrowCenterPosition - arrowWidth / 2))); 190 | } 191 | nodes.addArc( 192 | Rect.fromLTRB( 193 | left, top, left + borderRadius * 2, top + borderRadius * 2), 194 | pi, 195 | pi / 2); 196 | 197 | return DynamicPath(nodes: nodes, size: size); 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /lib/src/shape_borders/circle.dart: -------------------------------------------------------------------------------- 1 | import 'package:morphable_shape/src/common_includes.dart'; 2 | 3 | ///Circle shape 4 | class CircleShapeBorder extends OutlinedShapeBorder { 5 | const CircleShapeBorder({ 6 | DynamicBorderSide border = DynamicBorderSide.none, 7 | }) : super(border: border); 8 | 9 | CircleShapeBorder.fromJson(Map map) 10 | : super( 11 | border: parseDynamicBorderSide(map["border"]) ?? 12 | DynamicBorderSide.none); 13 | 14 | Map toJson() { 15 | Map rst = {"type": "Circle"}; 16 | rst.addAll(super.toJson()); 17 | return rst; 18 | } 19 | 20 | CircleShapeBorder copyWith({ 21 | DynamicBorderSide? border, 22 | }) { 23 | return CircleShapeBorder( 24 | border: border ?? this.border, 25 | ); 26 | } 27 | 28 | bool isSameMorphGeometry(MorphableShapeBorder shape) { 29 | return shape is CircleShapeBorder || 30 | shape is RectangleShapeBorder || 31 | shape is RoundedRectangleShapeBorder; 32 | } 33 | 34 | DynamicPath generateOuterDynamicPath(Rect rect) { 35 | final size = rect.size; 36 | 37 | List nodes = []; 38 | 39 | nodes.addArc( 40 | Rect.fromCenter( 41 | center: Offset(rect.width / 2.0, rect.height / 2.0), 42 | width: rect.width, 43 | height: rect.height, 44 | ), 45 | -pi / 2, 46 | pi / 2, 47 | splitTimes: 1); 48 | nodes.addArc( 49 | Rect.fromCenter( 50 | center: Offset(rect.width / 2.0, rect.height / 2.0), 51 | width: rect.width, 52 | height: rect.height, 53 | ), 54 | 0, 55 | pi / 2, 56 | splitTimes: 1); 57 | nodes.addArc( 58 | Rect.fromCenter( 59 | center: Offset(rect.width / 2.0, rect.height / 2.0), 60 | width: rect.width, 61 | height: rect.height, 62 | ), 63 | pi / 2, 64 | pi / 2, 65 | splitTimes: 1); 66 | nodes.addArc( 67 | Rect.fromCenter( 68 | center: Offset(rect.width / 2.0, rect.height / 2.0), 69 | width: rect.width, 70 | height: rect.height, 71 | ), 72 | pi, 73 | pi / 2, 74 | splitTimes: 1); 75 | 76 | return DynamicPath(nodes: nodes, size: size); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /lib/src/shape_borders/path.dart: -------------------------------------------------------------------------------- 1 | import 'package:morphable_shape/src/common_includes.dart'; 2 | 3 | ///A Shape made from a path with straight or cubic Bezier lines 4 | ///possible for future implementation of freeform lines or import shapes from SVG 5 | class PathShapeBorder extends OutlinedShapeBorder { 6 | final DynamicPath path; 7 | 8 | const PathShapeBorder( 9 | {DynamicBorderSide border = DynamicBorderSide.none, required this.path}) 10 | : super(border: border); 11 | 12 | PathShapeBorder.fromJson(Map map) 13 | : path = parseDynamicPath(map["path"]) ?? 14 | DynamicPath(size: Size.zero, nodes: []), 15 | super( 16 | border: parseDynamicBorderSide(map["border"]) ?? 17 | DynamicBorderSide.none); 18 | 19 | Map toJson() { 20 | Map rst = {"type": "Path"}; 21 | rst.addAll(super.toJson()); 22 | rst["path"] = path.toJson(); 23 | return rst; 24 | } 25 | 26 | PathShapeBorder copyWith({ 27 | DynamicPath? path, 28 | DynamicBorderSide? border, 29 | }) { 30 | return PathShapeBorder( 31 | path: path ?? this.path, border: border ?? this.border); 32 | } 33 | 34 | bool isSameMorphGeometry(MorphableShapeBorder shape) { 35 | return shape is PathShapeBorder && this.path == shape.path; 36 | } 37 | 38 | DynamicPath generateOuterDynamicPath(Rect rect) { 39 | return path..resize(rect.size); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /lib/src/shape_borders/polygon.dart: -------------------------------------------------------------------------------- 1 | import 'package:morphable_shape/src/common_includes.dart'; 2 | 3 | ///Polygon shape with different number of sides, corner radius and corner style 4 | class PolygonShapeBorder extends OutlinedShapeBorder { 5 | final int sides; 6 | final Dimension cornerRadius; 7 | final CornerStyle cornerStyle; 8 | 9 | const PolygonShapeBorder( 10 | {this.sides = 5, 11 | this.cornerStyle = CornerStyle.rounded, 12 | this.cornerRadius = const Length(0), 13 | border = DynamicBorderSide.none}) 14 | : assert(sides >= 3), 15 | super(border: border); 16 | 17 | PolygonShapeBorder.fromJson(Map map) 18 | : cornerStyle = 19 | parseCornerStyle(map["cornerStyle"]) ?? CornerStyle.rounded, 20 | cornerRadius = parseDimension(map["cornerRadius"]) ?? Length(0), 21 | sides = map["sides"] ?? 5, 22 | super( 23 | border: parseDynamicBorderSide(map["border"]) ?? 24 | DynamicBorderSide.none); 25 | 26 | Map toJson() { 27 | Map rst = {"type": "Polygon"}; 28 | rst.addAll(super.toJson()); 29 | rst["sides"] = sides; 30 | rst["cornerRadius"] = cornerRadius.toJson(); 31 | rst["cornerStyle"] = cornerStyle.toJson(); 32 | return rst; 33 | } 34 | 35 | PolygonShapeBorder copyWith({ 36 | CornerStyle? cornerStyle, 37 | Dimension? cornerRadius, 38 | int? sides, 39 | DynamicBorderSide? border, 40 | }) { 41 | return PolygonShapeBorder( 42 | border: border ?? this.border, 43 | cornerStyle: cornerStyle ?? this.cornerStyle, 44 | sides: sides ?? this.sides, 45 | cornerRadius: cornerRadius ?? this.cornerRadius, 46 | ); 47 | } 48 | 49 | bool isSameMorphGeometry(MorphableShapeBorder shape) { 50 | return shape is PolygonShapeBorder && this.sides == shape.sides; 51 | } 52 | 53 | DynamicPath generateOuterDynamicPath(Rect rect) { 54 | List nodes = []; 55 | 56 | final double alpha = (2.0 * pi / sides) / 2; 57 | double scale = min(rect.width, rect.height) / 2; 58 | final double centerX = scale; 59 | final double centerY = scale; 60 | double cornerRadius = this.cornerRadius.toPX(constraint: scale); 61 | double borderWidth = 0.0; 62 | 63 | borderWidth = borderWidth.clamp(0, scale * cos(alpha)); 64 | cornerRadius = cornerRadius.clamp(0, scale * cos(alpha)); 65 | 66 | double arcCenterRadius = 0; 67 | 68 | switch (cornerStyle) { 69 | case CornerStyle.rounded: 70 | arcCenterRadius = scale - cornerRadius / sin(pi / 2 - alpha); 71 | break; 72 | case CornerStyle.concave: 73 | case CornerStyle.cutout: 74 | arcCenterRadius = scale - 75 | cornerRadius / sin(pi / 2 - alpha) + 76 | borderWidth / sin(alpha); 77 | arcCenterRadius = arcCenterRadius.clamp(0.0, scale); 78 | cornerRadius -= borderWidth / tan(alpha); 79 | cornerRadius = cornerRadius.clamp(0.0, scale); 80 | break; 81 | 82 | case CornerStyle.straight: 83 | cornerRadius -= 84 | borderWidth * (1 - cos(alpha)) / sin(alpha) / tan(alpha); 85 | cornerRadius = cornerRadius.clamp(0.0, scale); 86 | arcCenterRadius = scale - cornerRadius / sin(pi / 2 - alpha); 87 | arcCenterRadius = arcCenterRadius.clamp(0.0, scale); 88 | break; 89 | } 90 | 91 | double startAngle = -pi / 2; 92 | 93 | for (int i = 0; i < sides; i++) { 94 | double cornerAngle = startAngle + 2 * alpha * i; 95 | double arcCenterX = (centerX + arcCenterRadius * cos(cornerAngle)); 96 | double arcCenterY = (centerY + arcCenterRadius * sin(cornerAngle)); 97 | Offset start = arcToCubicBezier( 98 | Rect.fromCircle( 99 | center: Offset(arcCenterX, arcCenterY), radius: cornerRadius), 100 | cornerAngle - alpha, 101 | 2 * alpha, 102 | splitTimes: 1) 103 | .first; 104 | Offset end = arcToCubicBezier( 105 | Rect.fromCircle( 106 | center: Offset(arcCenterX, arcCenterY), radius: cornerRadius), 107 | cornerAngle - alpha, 108 | 2 * alpha, 109 | splitTimes: 1) 110 | .last; 111 | 112 | switch (cornerStyle) { 113 | case CornerStyle.concave: 114 | nodes.addStyledCorner(Offset(arcCenterX, arcCenterY), cornerRadius, 115 | cornerAngle - alpha, 2 * alpha, 116 | style: CornerStyle.concave, splitTimes: 1); 117 | break; 118 | case CornerStyle.rounded: 119 | nodes.addArc( 120 | Rect.fromCircle( 121 | center: Offset(arcCenterX, arcCenterY), radius: cornerRadius), 122 | cornerAngle - alpha, 123 | 2 * alpha, 124 | splitTimes: 1); 125 | break; 126 | 127 | case CornerStyle.straight: 128 | nodes.add(DynamicNode(position: start)); 129 | nodes.add(DynamicNode(position: (start + end) / 2)); 130 | nodes.add(DynamicNode(position: end)); 131 | break; 132 | case CornerStyle.cutout: 133 | nodes.add(DynamicNode(position: start)); 134 | nodes.add(DynamicNode(position: Offset(arcCenterX, arcCenterY))); 135 | nodes.add(DynamicNode(position: end)); 136 | break; 137 | } 138 | //} 139 | } 140 | 141 | return DynamicPath(size: Size(2 * scale, 2 * scale), nodes: nodes) 142 | ..resize(rect.size); 143 | } 144 | 145 | ///used for implementing filled color polygon, works for a rectangle box but 146 | ///streches with rectangular boxes, revert back to border line polygon for now... 147 | /* 148 | @override 149 | List borderFillColors() { 150 | int degeneracy = 1; 151 | List colors = border.colors.extendColorsToLength(sides); 152 | List rst = []; 153 | for (int i = 0; i < colors.length; i++) { 154 | rst.addAll(List.generate(2 * degeneracy + 1, (index) => colors[i])); 155 | } 156 | 157 | return rotateList(rst, -degeneracy).cast(); 158 | } 159 | 160 | 161 | @override 162 | List borderFillGradients() { 163 | 164 | return List.generate(3*sides, (index) => null); 165 | } 166 | 167 | DynamicPath generateInnerDynamicPath(Rect rect) { 168 | List nodes = []; 169 | 170 | final double alpha = (2.0 * pi / sides) / 2; 171 | double scale = min(rect.width, rect.height) / 2; 172 | final double centerX = scale; 173 | final double centerY = scale; 174 | double cornerRadius = this.cornerRadius.toPX(constraintSize: scale); 175 | double borderWidth = this.border.width.toPX(constraintSize: scale); 176 | 177 | borderWidth = borderWidth.clamp(0, scale * cos(alpha)); 178 | cornerRadius = cornerRadius.clamp(0, scale * cos(alpha)); 179 | 180 | double arcCenterRadius = 181 | scale - max(cornerRadius, borderWidth) / sin(pi / 2 - alpha); 182 | cornerRadius = (cornerRadius - borderWidth).clamp(0, double.infinity); 183 | 184 | double startAngle = -pi / 2; 185 | 186 | for (int i = 0; i < sides; i++) { 187 | double cornerAngle = startAngle + 2 * alpha * i; 188 | double arcCenterX = (centerX + arcCenterRadius * cos(cornerAngle)); 189 | double arcCenterY = (centerY + arcCenterRadius * sin(cornerAngle)); 190 | Offset start = arcToCubicBezier( 191 | Rect.fromCircle( 192 | center: Offset(arcCenterX, arcCenterY), radius: cornerRadius), 193 | cornerAngle - alpha, 194 | 2 * alpha) 195 | .first; 196 | Offset end = arcToCubicBezier( 197 | Rect.fromCircle( 198 | center: Offset(arcCenterX, arcCenterY), radius: cornerRadius), 199 | cornerAngle - alpha, 200 | 2 * alpha) 201 | .last; 202 | 203 | switch (cornerStyle) { 204 | case CornerStyle.concave: 205 | Offset center = Offset(arcCenterX, arcCenterY); 206 | double radius = cornerRadius + borderWidth; 207 | double startAngle = cornerAngle - alpha; 208 | double sweepAngle = 2 * alpha; 209 | Offset newCenter = center + 210 | Offset.fromDirection((startAngle + sweepAngle / 2).clampAngle(), 211 | radius / cos(sweepAngle / 2)); 212 | double newSweep = (-(pi - sweepAngle)).clampAngle(); 213 | double newStart = 214 | -(pi - (startAngle + sweepAngle / 2) + newSweep / 2).clampAngle(); 215 | double newRadius = (cornerRadius + borderWidth) * tan(alpha); 216 | double delta = -asin((borderWidth / newRadius).clamp(0, 1)) 217 | .clamp(0.0, -newSweep / 2); 218 | 219 | nodes.addArc(Rect.fromCircle(center: newCenter, radius: newRadius), 220 | newStart + delta, min(-0.0000001, newSweep - 2 * delta), 221 | splitTimes: 1); 222 | break; 223 | case CornerStyle.rounded: 224 | nodes.addArc( 225 | Rect.fromCircle( 226 | center: Offset(arcCenterX, arcCenterY), radius: cornerRadius), 227 | cornerAngle - alpha, 228 | 2 * alpha, 229 | splitTimes: 1); 230 | break; 231 | 232 | case CornerStyle.straight: 233 | nodes.add(DynamicNode(position: start)); 234 | nodes.add(DynamicNode(position: (start + end) / 2)); 235 | nodes.add(DynamicNode(position: end)); 236 | break; 237 | case CornerStyle.cutout: 238 | nodes.add(DynamicNode(position: start)); 239 | nodes.add(DynamicNode(position: Offset(arcCenterX, arcCenterY))); 240 | nodes.add(DynamicNode(position: end)); 241 | break; 242 | } 243 | } 244 | 245 | return DynamicPath(size: Size(2 * scale, 2 * scale), nodes: nodes)..resize(rect.size); 246 | } 247 | */ 248 | 249 | } 250 | -------------------------------------------------------------------------------- /lib/src/shape_borders/rectangle.dart: -------------------------------------------------------------------------------- 1 | import 'package:morphable_shape/src/common_includes.dart'; 2 | import 'package:morphable_shape/src/ui_data_classes/dynamic_rectangle_styles.dart'; 3 | 4 | ///Rectangle shape with various corner style and radius for each corner 5 | class RectangleShapeBorder extends OutlinedShapeBorder { 6 | final RectangleCornerStyles cornerStyles; 7 | 8 | final DynamicBorderRadius borderRadius; 9 | 10 | const RectangleShapeBorder({ 11 | this.borderRadius = 12 | const DynamicBorderRadius.all(DynamicRadius.circular(Length(0))), 13 | this.cornerStyles = const RectangleCornerStyles.all(CornerStyle.rounded), 14 | border = DynamicBorderSide.none, 15 | }) : super(border: border); 16 | 17 | RectangleShapeBorder.fromJson(Map map) 18 | : borderRadius = parseDynamicBorderRadius(map["borderRadius"]) ?? 19 | DynamicBorderRadius.all(DynamicRadius.circular(Length(0))), 20 | this.cornerStyles = parseRectangleCornerStyle(map["cornerStyles"]) ?? 21 | RectangleCornerStyles.all(CornerStyle.rounded), 22 | super( 23 | border: parseDynamicBorderSide(map["border"]) ?? 24 | DynamicBorderSide.none); 25 | 26 | Map toJson() { 27 | Map rst = {"type": "Rectangle"}; 28 | rst.addAll(super.toJson()); 29 | rst["borderRadius"] = borderRadius.toJson(); 30 | rst["cornerStyles"] = cornerStyles.toJson(); 31 | return rst; 32 | } 33 | 34 | RectangleShapeBorder copyWith( 35 | {RectangleCornerStyles? cornerStyles, 36 | DynamicBorderSide? border, 37 | DynamicBorderRadius? borderRadius}) { 38 | return RectangleShapeBorder( 39 | cornerStyles: cornerStyles ?? this.cornerStyles, 40 | border: border ?? this.border, 41 | borderRadius: borderRadius ?? this.borderRadius); 42 | } 43 | 44 | bool isSameMorphGeometry(MorphableShapeBorder shape) { 45 | return shape is RectangleShapeBorder || 46 | shape is RoundedRectangleShapeBorder; 47 | } 48 | 49 | DynamicPath generateOuterDynamicPath(Rect rect) { 50 | Size size = rect.size; 51 | List nodes = []; 52 | 53 | final double left = rect.left; 54 | final double top = rect.top; 55 | final double bottom = rect.bottom; 56 | final double right = rect.right; 57 | 58 | BorderRadius borderRadius = this.borderRadius.toBorderRadius(size: size); 59 | 60 | double topLeftRadius = borderRadius.topLeft.x; 61 | double topRightRadius = borderRadius.topRight.x; 62 | 63 | double bottomLeftRadius = borderRadius.bottomLeft.x; 64 | double bottomRightRadius = borderRadius.bottomRight.x; 65 | 66 | double leftTopRadius = borderRadius.topLeft.y; 67 | double leftBottomRadius = borderRadius.bottomLeft.y; 68 | 69 | double rightTopRadius = borderRadius.topRight.y; 70 | double rightBottomRadius = borderRadius.bottomRight.y; 71 | 72 | double topTotal = topLeftRadius + topRightRadius; 73 | double bottomTotal = bottomLeftRadius + bottomRightRadius; 74 | double leftTotal = leftTopRadius + leftBottomRadius; 75 | double rightTotal = rightTopRadius + rightBottomRadius; 76 | 77 | if (max(topTotal, bottomTotal) > size.width || 78 | max(leftTotal, rightTotal) > size.height) { 79 | double resizeRatio = min(size.width / max(topTotal, bottomTotal), 80 | size.height / max(leftTotal, rightTotal)); 81 | 82 | topLeftRadius *= resizeRatio; 83 | topRightRadius *= resizeRatio; 84 | bottomLeftRadius *= resizeRatio; 85 | bottomRightRadius *= resizeRatio; 86 | 87 | leftTopRadius *= resizeRatio; 88 | rightTopRadius *= resizeRatio; 89 | leftBottomRadius *= resizeRatio; 90 | rightBottomRadius *= resizeRatio; 91 | } 92 | 93 | switch (cornerStyles.topRight) { 94 | case CornerStyle.rounded: 95 | nodes.addArc( 96 | Rect.fromCenter( 97 | center: Offset(right - topRightRadius, top + rightTopRadius), 98 | width: 2 * topRightRadius, 99 | height: 2 * rightTopRadius), 100 | -pi / 2, 101 | pi / 2, 102 | splitTimes: 1); 103 | break; 104 | case CornerStyle.straight: 105 | Offset start = Offset(right - topRightRadius, top); 106 | Offset end = Offset(right, top + rightTopRadius); 107 | nodes.add(DynamicNode(position: start)); 108 | nodes.add(DynamicNode(position: (start + end) / 2)); 109 | nodes.add(DynamicNode(position: end)); 110 | break; 111 | case CornerStyle.concave: 112 | nodes.addArc( 113 | Rect.fromCenter( 114 | center: Offset(right, top), 115 | width: 2 * topRightRadius, 116 | height: 2 * rightTopRadius), 117 | pi, 118 | -pi / 2, 119 | splitTimes: 1); 120 | break; 121 | case CornerStyle.cutout: 122 | nodes.add(DynamicNode(position: Offset(right - topRightRadius, top))); 123 | nodes.add(DynamicNode( 124 | position: Offset(right - topRightRadius, top + rightTopRadius))); 125 | nodes.add(DynamicNode(position: Offset(right, top + rightTopRadius))); 126 | } 127 | 128 | switch (cornerStyles.bottomRight) { 129 | case CornerStyle.rounded: 130 | nodes.addArc( 131 | Rect.fromCenter( 132 | center: Offset( 133 | right - bottomRightRadius, bottom - rightBottomRadius), 134 | width: 2 * bottomRightRadius, 135 | height: 2 * rightBottomRadius), 136 | 0, 137 | pi / 2, 138 | splitTimes: 1); 139 | break; 140 | case CornerStyle.straight: 141 | Offset start = Offset(right, bottom - rightBottomRadius); 142 | Offset end = Offset(right - bottomRightRadius, bottom); 143 | nodes.add(DynamicNode(position: start)); 144 | nodes.add(DynamicNode(position: (start + end) / 2)); 145 | nodes.add(DynamicNode(position: end)); 146 | break; 147 | case CornerStyle.concave: 148 | nodes.addArc( 149 | Rect.fromCenter( 150 | center: Offset(right, bottom), 151 | width: 2 * bottomRightRadius, 152 | height: 2 * rightBottomRadius), 153 | -pi / 2, 154 | -pi / 2, 155 | splitTimes: 1); 156 | break; 157 | case CornerStyle.cutout: 158 | nodes.add( 159 | DynamicNode(position: Offset(right, bottom - rightBottomRadius))); 160 | nodes.add(DynamicNode( 161 | position: 162 | Offset(right - bottomRightRadius, bottom - rightBottomRadius))); 163 | nodes.add( 164 | DynamicNode(position: Offset(right - bottomRightRadius, bottom))); 165 | } 166 | 167 | switch (cornerStyles.bottomLeft) { 168 | case CornerStyle.rounded: 169 | nodes.addArc( 170 | Rect.fromCenter( 171 | center: 172 | Offset(left + bottomLeftRadius, bottom - leftBottomRadius), 173 | width: 2 * bottomLeftRadius, 174 | height: 2 * leftBottomRadius), 175 | pi / 2, 176 | pi / 2, 177 | splitTimes: 1); 178 | break; 179 | case CornerStyle.straight: 180 | Offset start = Offset(left + bottomLeftRadius, bottom); 181 | Offset end = Offset(left, bottom - leftBottomRadius); 182 | nodes.add(DynamicNode(position: start)); 183 | nodes.add(DynamicNode(position: (start + end) / 2)); 184 | nodes.add(DynamicNode(position: end)); 185 | break; 186 | case CornerStyle.concave: 187 | nodes.addArc( 188 | Rect.fromCenter( 189 | center: Offset(left, bottom), 190 | width: 2 * bottomLeftRadius, 191 | height: 2 * leftBottomRadius), 192 | 0, 193 | -pi / 2, 194 | splitTimes: 1); 195 | break; 196 | case CornerStyle.cutout: 197 | nodes.add( 198 | DynamicNode(position: Offset(left + bottomLeftRadius, bottom))); 199 | nodes.add(DynamicNode( 200 | position: 201 | Offset(left + bottomLeftRadius, bottom - leftBottomRadius))); 202 | nodes.add( 203 | DynamicNode(position: Offset(left, bottom - leftBottomRadius))); 204 | } 205 | 206 | switch (cornerStyles.topLeft) { 207 | case CornerStyle.rounded: 208 | nodes.addArc( 209 | Rect.fromCenter( 210 | center: Offset(left + topLeftRadius, top + leftTopRadius), 211 | width: 2 * topLeftRadius, 212 | height: 2 * leftTopRadius), 213 | pi, 214 | pi / 2, 215 | splitTimes: 1); 216 | break; 217 | case CornerStyle.straight: 218 | Offset start = Offset(left, top + leftTopRadius); 219 | Offset end = Offset(left + topLeftRadius, top); 220 | nodes.add(DynamicNode(position: start)); 221 | nodes.add(DynamicNode(position: (start + end) / 2)); 222 | nodes.add(DynamicNode(position: end)); 223 | break; 224 | case CornerStyle.concave: 225 | nodes.addArc( 226 | Rect.fromCenter( 227 | center: Offset(left, top), 228 | width: 2 * topLeftRadius, 229 | height: 2 * leftTopRadius), 230 | pi / 2, 231 | -pi / 2, 232 | splitTimes: 1); 233 | break; 234 | case CornerStyle.cutout: 235 | nodes.add(DynamicNode(position: Offset(left, top + leftTopRadius))); 236 | nodes.add(DynamicNode( 237 | position: Offset(left + topLeftRadius, top + leftTopRadius))); 238 | nodes.add(DynamicNode(position: Offset(left + topLeftRadius, top))); 239 | } 240 | 241 | return DynamicPath(size: rect.size, nodes: nodes); 242 | } 243 | 244 | @override 245 | bool operator ==(Object other) { 246 | if (other.runtimeType != runtimeType) return false; 247 | return other is RectangleShapeBorder && 248 | other.border == border && 249 | other.cornerStyles == cornerStyles && 250 | other.borderRadius == borderRadius; 251 | } 252 | 253 | @override 254 | int get hashCode => Object.hash(border, cornerStyles, borderRadius); 255 | } 256 | -------------------------------------------------------------------------------- /lib/src/shape_borders/star.dart: -------------------------------------------------------------------------------- 1 | import 'package:morphable_shape/src/common_includes.dart'; 2 | 3 | ///Star shape, with different corner radius & style, inset radius & style. 4 | class StarShapeBorder extends OutlinedShapeBorder { 5 | final int corners; 6 | final Dimension inset; 7 | final Dimension cornerRadius; 8 | final Dimension insetRadius; 9 | final CornerStyle cornerStyle; 10 | final CornerStyle insetStyle; 11 | 12 | const StarShapeBorder({ 13 | this.corners = 4, 14 | this.inset = const Length(50, unit: LengthUnit.percent), 15 | this.cornerRadius = const Length(0), 16 | this.insetRadius = const Length(0), 17 | this.cornerStyle = CornerStyle.rounded, 18 | this.insetStyle = CornerStyle.rounded, 19 | border = DynamicBorderSide.none, 20 | }) : assert(corners >= 3), 21 | super(border: border); 22 | 23 | StarShapeBorder.fromJson(Map map) 24 | : cornerStyle = 25 | parseCornerStyle(map["cornerStyle"]) ?? CornerStyle.rounded, 26 | insetStyle = parseCornerStyle(map["insetStyle"]) ?? CornerStyle.rounded, 27 | corners = map["corners"] ?? 4, 28 | inset = parseDimension(map['inset']) ?? 29 | Length(50, unit: LengthUnit.percent), 30 | cornerRadius = parseDimension(map["cornerRadius"]) ?? Length(0), 31 | insetRadius = parseDimension(map["insetRadius"]) ?? Length(0), 32 | super( 33 | border: parseDynamicBorderSide(map["border"]) ?? 34 | DynamicBorderSide.none); 35 | 36 | Map toJson() { 37 | Map rst = {"type": "Star"}; 38 | rst.addAll(super.toJson()); 39 | rst["corners"] = corners; 40 | rst["inset"] = inset.toJson(); 41 | rst["cornerRadius"] = cornerRadius.toJson(); 42 | rst["insetRadius"] = insetRadius.toJson(); 43 | rst["cornerStyle"] = cornerStyle.toJson(); 44 | rst["insetStyle"] = insetStyle.toJson(); 45 | return rst; 46 | } 47 | 48 | StarShapeBorder copyWith({ 49 | int? corners, 50 | Dimension? inset, 51 | Dimension? cornerRadius, 52 | Dimension? insetRadius, 53 | CornerStyle? cornerStyle, 54 | CornerStyle? insetStyle, 55 | DynamicBorderSide? border, 56 | }) { 57 | return StarShapeBorder( 58 | corners: corners ?? this.corners, 59 | inset: inset ?? this.inset, 60 | cornerRadius: cornerRadius ?? this.cornerRadius, 61 | insetRadius: insetRadius ?? this.insetRadius, 62 | cornerStyle: cornerStyle ?? this.cornerStyle, 63 | insetStyle: insetStyle ?? this.insetStyle, 64 | border: border ?? this.border, 65 | ); 66 | } 67 | 68 | bool isSameMorphGeometry(MorphableShapeBorder shape) { 69 | return shape is StarShapeBorder && shape.corners == this.corners; 70 | } 71 | 72 | DynamicPath generateOuterDynamicPath(Rect rect) { 73 | List nodes = []; 74 | 75 | double scale = min(rect.width, rect.height) / 2; 76 | double cornerRadius = this.cornerRadius.toPX(constraint: scale / 2); 77 | double insetRadius = this.insetRadius.toPX(constraint: scale / 2); 78 | 79 | final int vertices = corners * 2; 80 | final double alpha = (2 * pi) / vertices; 81 | 82 | final double centerX = scale; 83 | final double centerY = scale; 84 | 85 | double inset = this.inset.toPX(constraint: scale); 86 | inset = inset.clamp(0.0, scale * 0.999); 87 | double sideLength = getThirdSideLength(scale, scale - inset, alpha); 88 | double beta = getThirdAngle(sideLength, scale, scale - inset); 89 | double gamma = alpha + beta; 90 | 91 | cornerRadius = cornerRadius.clamp(0, sideLength * tan(beta)); 92 | 93 | double avalSideLength = max(sideLength - cornerRadius / tan(beta), 0.0); 94 | if (gamma <= pi / 2) { 95 | insetRadius = insetRadius.clamp(0, avalSideLength * tan(gamma)); 96 | } else { 97 | insetRadius = insetRadius.clamp(0, avalSideLength * tan(pi - gamma)); 98 | } 99 | 100 | for (int i = 0; i < vertices; i++) { 101 | double r; 102 | double omega = -pi / 2 + alpha * i; 103 | if (i.isEven) { 104 | r = scale - cornerRadius / sin(beta); 105 | Offset center = 106 | Offset((r * cos(omega)) + centerX, (r * sin(omega)) + centerY); 107 | double sweepAngle = 2 * (pi / 2 - beta); 108 | Offset start = arcToCubicBezier( 109 | Rect.fromCircle(center: center, radius: cornerRadius), 110 | omega - sweepAngle / 2, 111 | sweepAngle) 112 | .first; 113 | Offset end = arcToCubicBezier( 114 | Rect.fromCircle(center: center, radius: cornerRadius), 115 | omega - sweepAngle / 2, 116 | sweepAngle) 117 | .last; 118 | 119 | switch (cornerStyle) { 120 | case CornerStyle.rounded: 121 | nodes.addArc(Rect.fromCircle(center: center, radius: cornerRadius), 122 | omega - sweepAngle / 2, sweepAngle, 123 | splitTimes: 1); 124 | break; 125 | case CornerStyle.concave: 126 | nodes.addStyledCorner( 127 | center, cornerRadius, omega - sweepAngle / 2, sweepAngle, 128 | splitTimes: 1, style: CornerStyle.concave); 129 | break; 130 | case CornerStyle.straight: 131 | nodes.add(DynamicNode(position: start)); 132 | nodes.add(DynamicNode(position: (start + end) / 2)); 133 | nodes.add(DynamicNode(position: end)); 134 | break; 135 | case CornerStyle.cutout: 136 | nodes.add(DynamicNode(position: start)); 137 | nodes.add(DynamicNode(position: center)); 138 | nodes.add(DynamicNode(position: end)); 139 | break; 140 | } 141 | } else { 142 | double sweepAngle = pi - 2 * gamma; 143 | if (gamma <= pi / 2) { 144 | r = scale - inset + insetRadius / sin(gamma); 145 | Offset center = 146 | Offset((r * cos(omega)) + centerX, (r * sin(omega)) + centerY); 147 | Offset start = arcToCubicBezier( 148 | Rect.fromCircle(center: center, radius: insetRadius), 149 | omega + sweepAngle / 2 + pi, 150 | -sweepAngle) 151 | .first; 152 | Offset end = arcToCubicBezier( 153 | Rect.fromCircle(center: center, radius: insetRadius), 154 | omega + sweepAngle / 2 + pi, 155 | -sweepAngle) 156 | .last; 157 | 158 | switch (insetStyle) { 159 | case CornerStyle.rounded: 160 | nodes.addArc(Rect.fromCircle(center: center, radius: insetRadius), 161 | omega + sweepAngle / 2 + pi, -sweepAngle, 162 | splitTimes: 1); 163 | break; 164 | case CornerStyle.concave: 165 | r = scale - inset; 166 | Offset center = Offset( 167 | (r * cos(omega)) + centerX, (r * sin(omega)) + centerY); 168 | double newSweep = ((pi - sweepAngle)).clampAngleWithin(); 169 | double newStart = 170 | ((omega + sweepAngle / 2 + pi + sweepAngle / 2) + 171 | newSweep / 2) 172 | .clampAngleWithin(); 173 | nodes.addArc( 174 | Rect.fromCircle( 175 | center: center, 176 | radius: insetRadius * tan(sweepAngle / 2)), 177 | newStart, 178 | newSweep, 179 | splitTimes: 1); 180 | break; 181 | case CornerStyle.straight: 182 | nodes.add(DynamicNode(position: start)); 183 | nodes.add(DynamicNode(position: (start + end) / 2)); 184 | nodes.add(DynamicNode(position: end)); 185 | break; 186 | case CornerStyle.cutout: 187 | nodes.add(DynamicNode(position: start)); 188 | nodes.add(DynamicNode(position: center)); 189 | nodes.add(DynamicNode(position: end)); 190 | break; 191 | } 192 | } else { 193 | sweepAngle = -sweepAngle; 194 | r = scale - inset - insetRadius / sin(gamma); 195 | Offset center = 196 | Offset((r * cos(omega)) + centerX, (r * sin(omega)) + centerY); 197 | Offset start = arcToCubicBezier( 198 | Rect.fromCircle(center: center, radius: insetRadius), 199 | omega - sweepAngle / 2, 200 | sweepAngle) 201 | .first; 202 | Offset end = arcToCubicBezier( 203 | Rect.fromCircle(center: center, radius: insetRadius), 204 | omega - sweepAngle / 2, 205 | sweepAngle) 206 | .last; 207 | 208 | switch (insetStyle) { 209 | case CornerStyle.rounded: 210 | nodes.addArc(Rect.fromCircle(center: center, radius: insetRadius), 211 | omega - sweepAngle / 2, sweepAngle, 212 | splitTimes: 1); 213 | break; 214 | case CornerStyle.concave: 215 | nodes.addStyledCorner( 216 | center, insetRadius, omega - sweepAngle / 2, sweepAngle, 217 | splitTimes: 1, style: CornerStyle.concave); 218 | 219 | break; 220 | case CornerStyle.straight: 221 | nodes.add(DynamicNode(position: start)); 222 | nodes.add(DynamicNode(position: (start + end) / 2)); 223 | nodes.add(DynamicNode(position: end)); 224 | break; 225 | case CornerStyle.cutout: 226 | nodes.add(DynamicNode(position: start)); 227 | nodes.add(DynamicNode(position: center)); 228 | nodes.add(DynamicNode(position: end)); 229 | break; 230 | } 231 | } 232 | } 233 | } 234 | 235 | return DynamicPath(size: Size(2 * scale, 2 * scale), nodes: nodes) 236 | ..resize(rect.size); 237 | } 238 | } 239 | -------------------------------------------------------------------------------- /lib/src/shape_borders/trapezoid.dart: -------------------------------------------------------------------------------- 1 | import 'package:morphable_shape/src/common_includes.dart'; 2 | 3 | ///A trapezoid shape 4 | class TrapezoidShapeBorder extends OutlinedShapeBorder { 5 | final ShapeSide side; 6 | final Dimension inset; 7 | 8 | const TrapezoidShapeBorder( 9 | {this.side = ShapeSide.bottom, 10 | this.inset = const Length(20, unit: LengthUnit.percent), 11 | DynamicBorderSide border = DynamicBorderSide.none}) 12 | : super(border: border); 13 | 14 | TrapezoidShapeBorder.fromJson(Map map) 15 | : side = parseShapeSide(map["side"]) ?? ShapeSide.bottom, 16 | inset = parseDimension(map['inset']) ?? 17 | Length(20, unit: LengthUnit.percent), 18 | super( 19 | border: parseDynamicBorderSide(map["border"]) ?? 20 | DynamicBorderSide.none); 21 | 22 | Map toJson() { 23 | Map rst = {"type": "Trapezoid"}; 24 | rst.addAll(super.toJson()); 25 | rst["inset"] = inset.toJson(); 26 | rst["side"] = side.toJson(); 27 | return rst; 28 | } 29 | 30 | TrapezoidShapeBorder copyWith({ 31 | ShapeSide? side, 32 | Dimension? inset, 33 | DynamicBorderSide? border, 34 | }) { 35 | return TrapezoidShapeBorder( 36 | side: side ?? this.side, 37 | inset: inset ?? this.inset, 38 | border: border ?? this.border, 39 | ); 40 | } 41 | 42 | bool isSameMorphGeometry(MorphableShapeBorder shape) { 43 | return shape is TrapezoidShapeBorder; 44 | } 45 | 46 | DynamicPath generateOuterDynamicPath(Rect rect) { 47 | List nodes = []; 48 | 49 | Size size = rect.size; 50 | 51 | double inset; 52 | if (side.isHorizontal) { 53 | inset = this.inset.toPX(constraint: size.width).clamp(0, size.width / 2); 54 | } else { 55 | inset = 56 | this.inset.toPX(constraint: size.height).clamp(0, size.height / 2); 57 | } 58 | 59 | switch (side) { 60 | case ShapeSide.top: 61 | { 62 | nodes.add(DynamicNode(position: Offset(inset, 0))); 63 | nodes.add(DynamicNode(position: Offset(size.width - inset, 0))); 64 | 65 | nodes.add(DynamicNode(position: Offset(size.width, size.height))); 66 | nodes.add(DynamicNode(position: Offset(0, size.height))); 67 | } 68 | break; 69 | case ShapeSide.bottom: 70 | { 71 | nodes.add(DynamicNode(position: Offset(0, 0))); 72 | nodes.add(DynamicNode(position: Offset(size.width, 0))); 73 | nodes.add( 74 | DynamicNode(position: Offset(size.width - inset, size.height))); 75 | nodes.add(DynamicNode(position: Offset(inset, size.height))); 76 | } 77 | break; 78 | case ShapeSide.left: 79 | { 80 | nodes.add(DynamicNode(position: Offset(0, inset))); 81 | nodes.add(DynamicNode(position: Offset(size.width, 0))); 82 | nodes.add(DynamicNode(position: Offset(size.width, size.height))); 83 | nodes.add(DynamicNode(position: Offset(0, size.height - inset))); 84 | } 85 | break; 86 | case ShapeSide.right: 87 | { 88 | nodes.add(DynamicNode(position: Offset(0, 0))); 89 | nodes.add(DynamicNode(position: Offset(size.width, inset))); 90 | nodes.add( 91 | DynamicNode(position: Offset(size.width, size.height - inset))); 92 | nodes.add(DynamicNode(position: Offset(0, size.height))); 93 | } 94 | break; 95 | } 96 | 97 | return DynamicPath(size: size, nodes: nodes); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /lib/src/shape_borders/triangle.dart: -------------------------------------------------------------------------------- 1 | import 'package:morphable_shape/src/common_includes.dart'; 2 | 3 | ///triangle shape defined by the three vertices 4 | class TriangleShapeBorder extends OutlinedShapeBorder { 5 | final DynamicOffset point1; 6 | final DynamicOffset point2; 7 | final DynamicOffset point3; 8 | 9 | const TriangleShapeBorder( 10 | {this.point1 = const DynamicOffset( 11 | const Length(0, unit: LengthUnit.percent), 12 | const Length(0, unit: LengthUnit.percent)), 13 | this.point2 = const DynamicOffset( 14 | const Length(100, unit: LengthUnit.percent), 15 | const Length(0, unit: LengthUnit.percent)), 16 | this.point3 = const DynamicOffset( 17 | const Length(50, unit: LengthUnit.percent), 18 | const Length(100, unit: LengthUnit.percent)), 19 | DynamicBorderSide border = DynamicBorderSide.none}) 20 | : super(border: border); 21 | 22 | TriangleShapeBorder.fromJson(Map map) 23 | : point1 = parseDynamicOffset(map["point1"]) ?? 24 | DynamicOffset(const Length(0, unit: LengthUnit.percent), 25 | const Length(0, unit: LengthUnit.percent)), 26 | point2 = parseDynamicOffset(map["point2"]) ?? 27 | DynamicOffset(const Length(100, unit: LengthUnit.percent), 28 | const Length(0, unit: LengthUnit.percent)), 29 | point3 = parseDynamicOffset(map["point3"]) ?? 30 | DynamicOffset(const Length(50, unit: LengthUnit.percent), 31 | const Length(100, unit: LengthUnit.percent)), 32 | super( 33 | border: parseDynamicBorderSide(map["border"]) ?? 34 | DynamicBorderSide.none); 35 | 36 | Map toJson() { 37 | Map rst = {"type": "Triangle"}; 38 | rst.addAll(super.toJson()); 39 | rst["point1"] = point1.toJson(); 40 | rst["point2"] = point2.toJson(); 41 | rst["point3"] = point3.toJson(); 42 | return rst; 43 | } 44 | 45 | TriangleShapeBorder copyWith({ 46 | DynamicOffset? point1, 47 | DynamicOffset? point2, 48 | DynamicOffset? point3, 49 | DynamicBorderSide? border, 50 | }) { 51 | return TriangleShapeBorder( 52 | point1: point1 ?? this.point1, 53 | point2: point2 ?? this.point2, 54 | point3: point3 ?? this.point3, 55 | border: border ?? this.border, 56 | ); 57 | } 58 | 59 | bool isSameMorphGeometry(MorphableShapeBorder shape) { 60 | return shape is TriangleShapeBorder; 61 | } 62 | 63 | DynamicPath generateOuterDynamicPath(Rect rect) { 64 | List nodes = []; 65 | 66 | Size size = rect.size; 67 | final width = rect.width; 68 | final height = rect.height; 69 | 70 | Offset point3 = this 71 | .point3 72 | .toOffset(size: size) 73 | .clamp(Offset.zero, Offset(width, height)); 74 | Offset point2 = this 75 | .point2 76 | .toOffset(size: size) 77 | .clamp(Offset.zero, Offset(width, height)); 78 | Offset point1 = this 79 | .point1 80 | .toOffset(size: size) 81 | .clamp(Offset.zero, Offset(width, height)); 82 | 83 | Offset center = (point1 + point2 + point3) / 3; 84 | 85 | List points = [ 86 | point1, 87 | point2, 88 | point3 89 | ]..sort((a, b) => (a - center).direction.compareTo((b - center).direction)); 90 | 91 | points.forEach((element) { 92 | nodes.add(DynamicNode(position: element)); 93 | }); 94 | 95 | return DynamicPath(size: rect.size, nodes: nodes); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /lib/src/ui_data_classes/corner_style.dart: -------------------------------------------------------------------------------- 1 | ///corner styles for rectangle, star, polygon, etc... 2 | enum CornerStyle { 3 | rounded, 4 | concave, 5 | straight, 6 | cutout, 7 | } 8 | -------------------------------------------------------------------------------- /lib/src/ui_data_classes/dynamic_border_radius.dart: -------------------------------------------------------------------------------- 1 | import 'package:morphable_shape/src/common_includes.dart'; 2 | 3 | class DynamicRadius { 4 | const DynamicRadius.circular(Length radius) : this.elliptical(radius, radius); 5 | 6 | /// Constructs an elliptical radius with the given radii. 7 | const DynamicRadius.elliptical(this.x, this.y); 8 | 9 | static const DynamicRadius zero = DynamicRadius.circular(const Length(0)); 10 | 11 | DynamicRadius.fromJson(Map map) 12 | : x = parseDimension(map["x"]) ?? 0.toPXLength, 13 | y = parseDimension(map["y"]) ?? 0.toPXLength; 14 | 15 | Map toJson() { 16 | Map map = {}; 17 | map["x"] = x.toJson(); 18 | map["y"] = y.toJson(); 19 | return map; 20 | } 21 | 22 | /// The radius value on the horizontal axis. 23 | final Dimension x; 24 | 25 | /// The radius value on the vertical axis. 26 | final Dimension y; 27 | 28 | DynamicRadius copyWith({ 29 | Dimension? x, 30 | Dimension? y, 31 | }) { 32 | return DynamicRadius.elliptical( 33 | x ?? this.x, 34 | y ?? this.y, 35 | ); 36 | } 37 | 38 | Radius toRadius({Size? size, Size? screenSize}) { 39 | return Radius.elliptical( 40 | x.toPX(constraint: size?.width, screenSize: screenSize), 41 | y.toPX(constraint: size?.height, screenSize: screenSize)); 42 | } 43 | 44 | @override 45 | bool operator ==(Object other) { 46 | if (identical(this, other)) return true; 47 | if (runtimeType != other.runtimeType) return false; 48 | 49 | return other is DynamicRadius && other.x == x && other.y == y; 50 | } 51 | 52 | @override 53 | int get hashCode => Object.hash(x, y); 54 | } 55 | 56 | class DynamicBorderRadius { 57 | const DynamicBorderRadius.all(DynamicRadius radius) 58 | : this.only( 59 | topLeft: radius, 60 | topRight: radius, 61 | bottomLeft: radius, 62 | bottomRight: radius, 63 | ); 64 | 65 | const DynamicBorderRadius.only({ 66 | this.topLeft = const DynamicRadius.circular(Length(0)), 67 | this.topRight = const DynamicRadius.circular(Length(0)), 68 | this.bottomLeft = const DynamicRadius.circular(Length(0)), 69 | this.bottomRight = const DynamicRadius.circular(Length(0)), 70 | }); 71 | 72 | DynamicBorderRadius.fromJson(Map map) 73 | : topLeft = DynamicRadius.fromJson(map["topLeft"]), 74 | topRight = DynamicRadius.fromJson(map["topRight"]), 75 | bottomLeft = DynamicRadius.fromJson(map["bottomLeft"]), 76 | bottomRight = DynamicRadius.fromJson(map["bottomRight"]); 77 | 78 | Map toJson() { 79 | Map map = {}; 80 | map["topLeft"] = topLeft.toJson(); 81 | map["topRight"] = topRight.toJson(); 82 | map["bottomLeft"] = bottomLeft.toJson(); 83 | map["bottomRight"] = bottomRight.toJson(); 84 | return map; 85 | } 86 | 87 | DynamicBorderRadius copyWith({ 88 | DynamicRadius? topLeft, 89 | DynamicRadius? topRight, 90 | DynamicRadius? bottomLeft, 91 | DynamicRadius? bottomRight, 92 | }) { 93 | return DynamicBorderRadius.only( 94 | topLeft: topLeft ?? this.topLeft, 95 | topRight: topRight ?? this.topRight, 96 | bottomLeft: bottomLeft ?? this.bottomLeft, 97 | bottomRight: bottomRight ?? this.bottomRight, 98 | ); 99 | } 100 | 101 | final DynamicRadius topLeft; 102 | final DynamicRadius topRight; 103 | final DynamicRadius bottomLeft; 104 | final DynamicRadius bottomRight; 105 | 106 | BorderRadius toBorderRadius({Size? size, Size? screenSize}) { 107 | return BorderRadius.only( 108 | topLeft: topLeft.toRadius(size: size, screenSize: screenSize), 109 | topRight: topRight.toRadius(size: size, screenSize: screenSize), 110 | bottomLeft: bottomLeft.toRadius(size: size, screenSize: screenSize), 111 | bottomRight: bottomRight.toRadius(size: size, screenSize: screenSize), 112 | ); 113 | } 114 | 115 | @override 116 | bool operator ==(Object other) { 117 | if (identical(this, other)) return true; 118 | if (runtimeType != other.runtimeType) return false; 119 | 120 | return other is DynamicBorderRadius && 121 | other.topLeft == topLeft && 122 | other.topRight == topRight && 123 | other.bottomLeft == bottomLeft && 124 | other.bottomRight == bottomRight; 125 | } 126 | 127 | @override 128 | int get hashCode => Object.hash(topLeft, topRight, bottomLeft, bottomRight); 129 | } 130 | -------------------------------------------------------------------------------- /lib/src/ui_data_classes/dynamic_border_side.dart: -------------------------------------------------------------------------------- 1 | import 'dart:ui'; 2 | 3 | import 'package:morphable_shape/src/common_includes.dart'; 4 | 5 | ///An enhanced BorderSide class. 6 | ///Supports gradient, different strokeJoin and strokeCap 7 | ///Also supports partial drawing by changing the begin, end, and shift 8 | 9 | class DynamicBorderSide { 10 | const DynamicBorderSide({ 11 | this.color = const Color(0xFF000000), 12 | this.width = 1.0, 13 | this.style = BorderStyle.solid, 14 | this.gradient, 15 | this.begin, 16 | this.end, 17 | this.shift, 18 | this.strokeJoin = StrokeJoin.miter, 19 | this.strokeCap = StrokeCap.butt, 20 | }); 21 | 22 | DynamicBorderSide.fromJson(Map map) 23 | : color = parseColor(map["color"]) ?? Color(0xFF000000), 24 | gradient = parseGradient(map["gradient"]), 25 | width = map["width"].toDouble() ?? 1.0, 26 | style = parseBorderStyle(map["style"]) ?? BorderStyle.solid, 27 | begin = 28 | parseDimension(map["begin"]) ?? Length(0, unit: LengthUnit.percent), 29 | end = 30 | parseDimension(map["end"]) ?? Length(100, unit: LengthUnit.percent), 31 | shift = 32 | parseDimension(map["shift"]) ?? Length(0, unit: LengthUnit.percent), 33 | strokeCap = parseStrokeCap(map["strokeCap"]) ?? StrokeCap.square, 34 | strokeJoin = parseStrokeJoin(map["strokeJoin"]) ?? StrokeJoin.miter; 35 | 36 | Map toJson() { 37 | Map rst = {}; 38 | rst["color"] = color.toJson(); 39 | rst.updateNotNull("gradient", gradient?.toJson()); 40 | rst["width"] = width; 41 | rst["style"] = style.toJson(); 42 | rst.updateNotNull("begin", begin?.toJson()); 43 | rst.updateNotNull("end", end?.toJson()); 44 | rst.updateNotNull("shift", end?.toJson()); 45 | rst.updateNotNull("strokeCap", strokeCap.toJson()); 46 | rst.updateNotNull("strokeJoin", strokeJoin.toJson()); 47 | return rst; 48 | } 49 | 50 | /// The color of this side of the border. 51 | final Color color; 52 | 53 | final Gradient? gradient; 54 | 55 | /// The width of this side of the border, in logical pixels. 56 | /// 57 | /// Setting width to 0.0 will result in a hairline border. This means that 58 | /// the border will have the width of one physical pixel. Also, hairline 59 | /// rendering takes shortcuts when the path overlaps a pixel more than once. 60 | /// This means that it will render faster than otherwise, but it might 61 | /// double-hit pixels, giving it a slightly darker/lighter result. 62 | /// 63 | /// To omit the border entirely, set the [style] to [BorderStyle.none]. 64 | final double width; 65 | 66 | /// The style of this side of the border. 67 | /// 68 | /// To omit a side, set [style] to [BorderStyle.none]. This skips 69 | /// painting the border, but the border still has a [width]. 70 | final BorderStyle style; 71 | 72 | final Dimension? begin; 73 | final Dimension? end; 74 | final Dimension? shift; 75 | 76 | final StrokeJoin strokeJoin; 77 | final StrokeCap strokeCap; 78 | 79 | /// A hairline black border that is not rendered. 80 | static const DynamicBorderSide none = 81 | DynamicBorderSide(width: 0.0, style: BorderStyle.none); 82 | 83 | DynamicBorderSide copyWith({ 84 | Color? color, 85 | Gradient? gradient, 86 | double? width, 87 | BorderStyle? style, 88 | Dimension? begin, 89 | Dimension? end, 90 | Dimension? shift, 91 | StrokeJoin? strokeJoin, 92 | StrokeCap? strokeCap, 93 | }) { 94 | return DynamicBorderSide( 95 | color: color ?? this.color, 96 | gradient: gradient ?? this.gradient, 97 | width: width ?? this.width, 98 | style: style ?? this.style, 99 | begin: begin ?? this.begin, 100 | end: end ?? this.end, 101 | shift: shift ?? this.shift, 102 | strokeCap: strokeCap ?? this.strokeCap, 103 | strokeJoin: strokeJoin ?? this.strokeJoin, 104 | ); 105 | } 106 | 107 | DynamicBorderSide scale(double t) { 108 | return DynamicBorderSide( 109 | color: color, 110 | gradient: gradient?.scale(t), 111 | width: max(0.0, width * t), 112 | style: t <= 0.0 ? BorderStyle.none : style, 113 | begin: begin, 114 | end: end, 115 | shift: shift, 116 | strokeJoin: strokeJoin, 117 | strokeCap: strokeCap, 118 | ); 119 | } 120 | 121 | static DynamicBorderSide lerp( 122 | DynamicBorderSide a, DynamicBorderSide b, double t) { 123 | if (t == 0.0) return a; 124 | if (t == 1.0) return b; 125 | final double width = lerpDouble(a.width, b.width, t) ?? 0.0; 126 | if (width < 0.0) return DynamicBorderSide.none; 127 | if (a.style == b.style) { 128 | return DynamicBorderSide( 129 | color: Color.lerp(a.color, b.color, t)!, 130 | gradient: Gradient.lerp(a.gradient, b.gradient, t), 131 | width: width, 132 | style: a.style, 133 | // == b.style 134 | begin: Dimension.lerp(a.begin, b.begin, t), 135 | end: a.end == null && b.end == null 136 | ? null 137 | : Dimension.lerp( 138 | a.end ?? 100.toPercentLength, b.end ?? 100.toPercentLength, t), 139 | shift: Dimension.lerp(a.shift, b.shift, t), 140 | strokeCap: t < 0.5 ? a.strokeCap : b.strokeCap, 141 | strokeJoin: t < 0.5 ? a.strokeJoin : b.strokeJoin, 142 | ); 143 | } 144 | Color colorA, colorB; 145 | switch (a.style) { 146 | case BorderStyle.solid: 147 | colorA = a.color; 148 | break; 149 | case BorderStyle.none: 150 | colorA = a.color.withAlpha(0x00); 151 | break; 152 | } 153 | switch (b.style) { 154 | case BorderStyle.solid: 155 | colorB = b.color; 156 | break; 157 | case BorderStyle.none: 158 | colorB = b.color.withAlpha(0x00); 159 | break; 160 | } 161 | return DynamicBorderSide( 162 | color: Color.lerp(colorA, colorB, t)!, 163 | gradient: Gradient.lerp(a.gradient, b.gradient, t), 164 | width: width, 165 | style: BorderStyle.solid, 166 | begin: Dimension.lerp(a.begin, b.begin, t), 167 | end: a.end == null && b.end == null 168 | ? null 169 | : Dimension.lerp( 170 | a.end ?? 100.toPercentLength, b.end ?? 100.toPercentLength, t), 171 | shift: Dimension.lerp(a.shift, b.shift, t), 172 | strokeCap: t < 0.5 ? a.strokeCap : b.strokeCap, 173 | strokeJoin: t < 0.5 ? a.strokeJoin : b.strokeJoin, 174 | ); 175 | } 176 | 177 | @override 178 | bool operator ==(Object other) { 179 | if (identical(this, other)) return true; 180 | if (other.runtimeType != runtimeType) return false; 181 | return other is DynamicBorderSide && 182 | other.color == color && 183 | other.gradient == gradient && 184 | other.width == width && 185 | other.style == style && 186 | other.begin == begin && 187 | other.end == end && 188 | other.shift == shift && 189 | other.strokeCap == strokeCap && 190 | other.strokeJoin == strokeJoin; 191 | } 192 | 193 | @override 194 | int get hashCode => Object.hash( 195 | color, gradient, width, style, begin, end, shift, strokeJoin, strokeCap); 196 | } 197 | -------------------------------------------------------------------------------- /lib/src/ui_data_classes/dynamic_offset.dart: -------------------------------------------------------------------------------- 1 | import 'package:morphable_shape/src/common_includes.dart'; 2 | 3 | ///An offset with two Dimension instance 4 | 5 | class DynamicOffset { 6 | /// Constructs an elliptical radius with the given radii. 7 | const DynamicOffset(this.dx, this.dy); 8 | 9 | Map toJson() { 10 | Map map = {}; 11 | map["dx"] = dx.toJson(); 12 | map["dy"] = dy.toJson(); 13 | return map; 14 | } 15 | 16 | static const DynamicOffset zero = 17 | DynamicOffset(const Length(0), const Length(0)); 18 | 19 | /// The radius value on the horizontal axis. 20 | final Dimension dx; 21 | 22 | /// The radius value on the vertical axis. 23 | final Dimension dy; 24 | 25 | DynamicOffset copyWith({ 26 | Dimension? dx, 27 | Dimension? dy, 28 | }) { 29 | return DynamicOffset( 30 | dx ?? this.dx, 31 | dy ?? this.dy, 32 | ); 33 | } 34 | 35 | Offset toOffset({Size? size, Size? screenSize}) { 36 | return Offset(dx.toPX(constraint: size?.width, screenSize: screenSize), 37 | dy.toPX(constraint: size?.height, screenSize: screenSize)); 38 | } 39 | 40 | @override 41 | int get hashCode => Object.hash(dx, dy); 42 | 43 | @override 44 | bool operator ==(dynamic other) { 45 | return other is DynamicOffset && dx == other.dx && dy == other.dy; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /lib/src/ui_data_classes/dynamic_rectangle_styles.dart: -------------------------------------------------------------------------------- 1 | import 'package:morphable_shape/src/common_includes.dart'; 2 | 3 | ///classes for configuring the border and corner of a rectangle 4 | class RectangleCornerStyles { 5 | final CornerStyle topLeft; 6 | final CornerStyle bottomLeft; 7 | final CornerStyle topRight; 8 | final CornerStyle bottomRight; 9 | 10 | const RectangleCornerStyles.only({ 11 | this.topLeft = CornerStyle.rounded, 12 | this.bottomLeft = CornerStyle.rounded, 13 | this.topRight = CornerStyle.rounded, 14 | this.bottomRight = CornerStyle.rounded, 15 | }); 16 | 17 | const RectangleCornerStyles.all(CornerStyle style) 18 | : topLeft = style, 19 | bottomLeft = style, 20 | topRight = style, 21 | bottomRight = style; 22 | 23 | static RectangleCornerStyles fromJson(Map map) { 24 | return RectangleCornerStyles.only( 25 | topLeft: parseCornerStyle(map['topLeft']) ?? CornerStyle.rounded, 26 | bottomLeft: parseCornerStyle(map['bottomLeft']) ?? CornerStyle.rounded, 27 | topRight: parseCornerStyle(map['topRight']) ?? CornerStyle.rounded, 28 | bottomRight: 29 | parseCornerStyle(map['bottomRight']) ?? CornerStyle.rounded); 30 | } 31 | 32 | Map toJson() { 33 | return { 34 | "topLeft": topLeft.toJson(), 35 | "bottomLeft": bottomLeft.toJson(), 36 | "topRight": topRight.toJson(), 37 | "bottomRight": bottomRight.toJson() 38 | }; 39 | } 40 | 41 | RectangleCornerStyles copyWith({ 42 | CornerStyle? topLeft, 43 | CornerStyle? bottomLeft, 44 | CornerStyle? topRight, 45 | CornerStyle? bottomRight, 46 | }) { 47 | return RectangleCornerStyles.only( 48 | topLeft: topLeft ?? this.topLeft, 49 | topRight: topRight ?? this.topRight, 50 | bottomLeft: bottomLeft ?? this.bottomLeft, 51 | bottomRight: bottomRight ?? this.bottomRight, 52 | ); 53 | } 54 | } 55 | 56 | class RectangleBorderSides { 57 | final DynamicBorderSide top; 58 | final DynamicBorderSide bottom; 59 | final DynamicBorderSide left; 60 | final DynamicBorderSide right; 61 | 62 | const RectangleBorderSides.only({ 63 | this.top = DynamicBorderSide.none, 64 | this.bottom = DynamicBorderSide.none, 65 | this.left = DynamicBorderSide.none, 66 | this.right = DynamicBorderSide.none, 67 | }); 68 | 69 | const RectangleBorderSides.all(DynamicBorderSide border) 70 | : top = border, 71 | bottom = border, 72 | left = border, 73 | right = border; 74 | 75 | const RectangleBorderSides.symmetric( 76 | {DynamicBorderSide horizontal = DynamicBorderSide.none, 77 | DynamicBorderSide vertical = DynamicBorderSide.none}) 78 | : top = horizontal, 79 | bottom = horizontal, 80 | left = vertical, 81 | right = vertical; 82 | 83 | static RectangleBorderSides fromJson(Map map) { 84 | return RectangleBorderSides.only( 85 | top: parseDynamicBorderSide(map['top']) ?? DynamicBorderSide.none, 86 | bottom: parseDynamicBorderSide(map['bottom']) ?? DynamicBorderSide.none, 87 | left: parseDynamicBorderSide(map['left']) ?? DynamicBorderSide.none, 88 | right: parseDynamicBorderSide(map['right']) ?? DynamicBorderSide.none, 89 | ); 90 | } 91 | 92 | Map toJson() { 93 | return { 94 | "top": top.toJson(), 95 | "bottom": bottom.toJson(), 96 | "left": left.toJson(), 97 | "right": right.toJson() 98 | }; 99 | } 100 | 101 | RectangleBorderSides copyWith({ 102 | DynamicBorderSide? top, 103 | DynamicBorderSide? bottom, 104 | DynamicBorderSide? left, 105 | DynamicBorderSide? right, 106 | }) { 107 | return RectangleBorderSides.only( 108 | top: top ?? this.top, 109 | bottom: bottom ?? this.bottom, 110 | left: left ?? this.left, 111 | right: right ?? this.right, 112 | ); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /lib/src/ui_data_classes/shape_shadow.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math' as math; 2 | import 'dart:ui' as ui show Shadow, lerpDouble; 3 | 4 | import 'package:flutter/foundation.dart'; 5 | import 'package:morphable_shape/src/common_includes.dart'; 6 | 7 | class ShapeShadow extends ui.Shadow { 8 | /// Creates a shape shadow. 9 | /// 10 | /// By default, the shadow is solid black with zero [offset], [blurRadius], 11 | /// and [spreadRadius]. 12 | /// If gradient is not null, the gradient will be used. 13 | const ShapeShadow({ 14 | Color color = const Color(0xFF000000), 15 | Offset offset = Offset.zero, 16 | double blurRadius = 0.0, 17 | this.spreadRadius = 0.0, 18 | this.gradient, 19 | this.blurStyle = BlurStyle.normal, 20 | }) : super(color: color, offset: offset, blurRadius: blurRadius); 21 | 22 | factory ShapeShadow.fromBoxShadow(BoxShadow source) { 23 | return ShapeShadow( 24 | color: source.color, 25 | offset: source.offset, 26 | blurRadius: source.blurRadius, 27 | spreadRadius: source.spreadRadius, 28 | blurStyle: source.blurStyle, 29 | ); 30 | } 31 | 32 | Map toJson() { 33 | Map rst = {}; 34 | rst.updateNotNull("color", color.toJson()); 35 | rst.updateNotNull("offset", offset.toJson()); 36 | rst.updateNotNull("blurRadius", blurRadius); 37 | rst.updateNotNull("gradient", gradient?.toJson()); 38 | rst.updateNotNull("spreadRadius", spreadRadius); 39 | //TODO: parse blurStyle 40 | //rst.updateNotNull("blurStyle", blurStyle) 41 | return rst; 42 | } 43 | 44 | /// The amount the box should be inflated prior to applying the blur. 45 | final double spreadRadius; 46 | 47 | ///This gradient will only be used by the ShadowedShape class. 48 | ///If used by other class, this gradient takes no effect 49 | final Gradient? gradient; 50 | 51 | final BlurStyle blurStyle; 52 | 53 | /// Create the [Paint] object that corresponds to this shadow description. 54 | /// 55 | /// The [offset] and [spreadRadius] are not represented in the [Paint] object. 56 | /// To honor those as well, the shape should be inflated by [spreadRadius] pixels 57 | /// in every direction and then translated by [offset] before being filled using 58 | /// this [Paint]. 59 | @override 60 | Paint toPaint() { 61 | final Paint result = Paint() 62 | ..color = color 63 | ..maskFilter = MaskFilter.blur(BlurStyle.normal, blurSigma); 64 | assert(() { 65 | if (debugDisableShadows) result.maskFilter = null; 66 | return true; 67 | }()); 68 | return result; 69 | } 70 | 71 | /// Returns a new box shadow with its offset, blurRadius, and spreadRadius scaled by the given factor. 72 | @override 73 | ShapeShadow scale(double factor) { 74 | return ShapeShadow( 75 | color: color, 76 | gradient: gradient, 77 | offset: offset * factor, 78 | blurRadius: (blurRadius * factor).clamp(0, double.infinity), 79 | spreadRadius: spreadRadius * factor, 80 | blurStyle: blurStyle, 81 | ); 82 | } 83 | 84 | /// Linearly interpolate between two box shadows. 85 | /// 86 | /// If either box shadow is null, this function linearly interpolates from a 87 | /// a box shadow that matches the other box shadow in color but has a zero 88 | /// offset and a zero blurRadius. 89 | /// 90 | /// {@macro dart.ui.shadow.lerp} 91 | static ShapeShadow? lerp(ShapeShadow? a, ShapeShadow? b, double t) { 92 | if (a == null && b == null) return null; 93 | if (a == null) return b!.scale(t); 94 | if (b == null) return a.scale(1.0 - t); 95 | 96 | return ShapeShadow( 97 | color: Color.lerp(a.color, b.color, t)!, 98 | gradient: Gradient.lerp(a.gradient, b.gradient, t), 99 | offset: Offset.lerp(a.offset, b.offset, t)!, 100 | blurRadius: ui 101 | .lerpDouble(a.blurRadius, b.blurRadius, t)! 102 | .clamp(0, double.infinity), 103 | spreadRadius: ui.lerpDouble(a.spreadRadius, b.spreadRadius, t)!, 104 | blurStyle: t < 0.5 ? a.blurStyle : b.blurStyle, 105 | ); 106 | } 107 | 108 | /// Linearly interpolate between two lists of box shadows. 109 | /// 110 | /// If the lists differ in length, excess items are lerped with null. 111 | /// 112 | /// {@macro dart.ui.shadow.lerp} 113 | static List? lerpList( 114 | List? a, List? b, double t) { 115 | if (a == null && b == null) return null; 116 | a ??= []; 117 | b ??= []; 118 | final int commonLength = math.min(a.length, b.length); 119 | return [ 120 | for (int i = 0; i < commonLength; i += 1) 121 | ShapeShadow.lerp(a[i], b[i], t)!, 122 | for (int i = commonLength; i < a.length; i += 1) a[i].scale(1.0 - t), 123 | for (int i = commonLength; i < b.length; i += 1) b[i].scale(t), 124 | ]; 125 | } 126 | 127 | @override 128 | bool operator ==(Object other) { 129 | if (identical(this, other)) return true; 130 | if (other.runtimeType != runtimeType) return false; 131 | return other is ShapeShadow && 132 | other.color == color && 133 | other.offset == offset && 134 | other.gradient == gradient && 135 | other.blurRadius == blurRadius && 136 | other.spreadRadius == spreadRadius && 137 | other.blurStyle == blurStyle; 138 | } 139 | 140 | @override 141 | int get hashCode => 142 | Object.hash(color, gradient, offset, blurRadius, spreadRadius, blurStyle); 143 | 144 | @override 145 | String toString() => 146 | 'BoxShadow($color, $gradient, $offset, ${debugFormatDouble(blurRadius)}, ${debugFormatDouble(spreadRadius)})'; 147 | } 148 | -------------------------------------------------------------------------------- /lib/src/ui_data_classes/shape_side.dart: -------------------------------------------------------------------------------- 1 | ///represent a shape feature at one of the four side of a rectangle 2 | enum ShapeSide { bottom, top, left, right } 3 | -------------------------------------------------------------------------------- /lib/src/utils/utils_extension_methods.dart: -------------------------------------------------------------------------------- 1 | import 'package:morphable_shape/src/common_includes.dart'; 2 | 3 | extension CornerStyleExtension on CornerStyle { 4 | String toJson() { 5 | return this.toString().stripFirstDot(); 6 | } 7 | 8 | bool get isConcave { 9 | return this == CornerStyle.concave || this == CornerStyle.cutout; 10 | } 11 | } 12 | 13 | extension ShapeSideExtension on ShapeSide { 14 | String toJson() { 15 | return this.toString().stripFirstDot(); 16 | } 17 | 18 | bool get isHorizontal { 19 | return this == ShapeSide.top || this == ShapeSide.bottom; 20 | } 21 | 22 | bool get isVertical { 23 | return !this.isHorizontal; 24 | } 25 | } 26 | 27 | extension OffsetExtension on Offset { 28 | Offset clamp(Offset lower, Offset upper) { 29 | return Offset(this.dx.clamp(lower.dx, max(upper.dx, lower.dx)), 30 | this.dy.clamp(lower.dy, max(upper.dy, lower.dy))); 31 | } 32 | 33 | Offset rotateAround({Offset pivot = Offset.zero, double angle = 0.0}) { 34 | double distance = (this - pivot).distance; 35 | double direction = (this - pivot).direction; 36 | return pivot + Offset.fromDirection(direction + angle, distance); 37 | } 38 | 39 | Offset roundWithPrecision(int N) { 40 | return Offset(this.dx.roundWithPrecision(N), this.dy.roundWithPrecision(N)); 41 | } 42 | } 43 | 44 | extension IndexedIterable on Iterable { 45 | Iterable mapIndexed(T Function(E e, int i) f) { 46 | var i = 0; 47 | return map((e) => f(e, i++)); 48 | } 49 | } 50 | 51 | extension NumListExtension on List { 52 | num total() { 53 | num total = 0; 54 | this.forEach((element) { 55 | total += element; 56 | }); 57 | return total; 58 | } 59 | } 60 | 61 | ///Every arc will be converted to cubic Bezier path(s) in this package 62 | extension addDynamicNodeExtension on List { 63 | void cubicTo(Offset x1, Offset x2, Offset x3) { 64 | if (this.isEmpty) { 65 | return; 66 | } 67 | this.last.next = x1; 68 | DynamicNode newNode = DynamicNode(position: x3, prev: x2); 69 | this.add(newNode); 70 | } 71 | 72 | void addArc(Rect rect, double startAngle, double sweepAngle, 73 | {int? splitTimes}) { 74 | ///configure the minSplitTimes to let some FilledColorShape have symmetric split at the rounded corners 75 | List points = 76 | arcToCubicBezier(rect, startAngle, sweepAngle, splitTimes: splitTimes); 77 | this.add(DynamicNode(position: points[0])); 78 | for (int i = 0; i < points.length; i += 4) { 79 | this.cubicTo(points[i + 1], points[i + 2], points[i + 3]); 80 | } 81 | } 82 | 83 | ///has to assume it is a circle, not an eclipse 84 | void addStyledCorner( 85 | Offset center, double radius, double startAngle, double sweepAngle, 86 | {CornerStyle style = CornerStyle.rounded, int? splitTimes}) { 87 | ///configure the minSplitTimes to let some FilledColorShape have symmetric split at the rounded corners 88 | List points = arcToCubicBezier( 89 | Rect.fromCircle(center: center, radius: radius), startAngle, sweepAngle, 90 | splitTimes: splitTimes); 91 | if (style == CornerStyle.rounded) { 92 | for (int i = 0; i < points.length; i += 4) { 93 | this.cubicTo(points[i + 1], points[i + 2], points[i + 3]); 94 | } 95 | } 96 | if (style == CornerStyle.straight) { 97 | this.add(DynamicNode(position: points.first)); 98 | this.add(DynamicNode(position: (points.first + points.last) / 2)); 99 | this.add(DynamicNode(position: points.last)); 100 | } 101 | if (style == CornerStyle.concave) { 102 | Offset newCenter = center + 103 | Offset.fromDirection((startAngle + sweepAngle / 2).clampAngleWithin(), 104 | radius / cos(sweepAngle / 2)); 105 | double newSweep = (-(pi - sweepAngle)).clampAngleWithin(); 106 | double newStart = -(pi - (startAngle + sweepAngle / 2) + newSweep / 2) 107 | .clampAngleWithin(); 108 | this.addArc( 109 | Rect.fromCircle( 110 | center: newCenter, radius: radius * tan(sweepAngle / 2)), 111 | newStart, 112 | newSweep, 113 | splitTimes: splitTimes); 114 | } 115 | if (style == CornerStyle.cutout) { 116 | this.add(DynamicNode(position: points.first)); 117 | this.add(DynamicNode(position: center)); 118 | this.add(DynamicNode(position: points.last)); 119 | } 120 | } 121 | } 122 | 123 | extension angleDoubleExtension on double { 124 | double toRadian() { 125 | return this / 180 * pi; 126 | } 127 | 128 | double toDegree() { 129 | return this / pi * 180; 130 | } 131 | 132 | double clampAngleWithin() { 133 | if (this > pi) return this - 2 * pi; 134 | if (this < -pi) return this + 2 * pi; 135 | return this; 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /lib/src/utils/utils_math_geometry.dart: -------------------------------------------------------------------------------- 1 | import 'package:morphable_shape/src/common_includes.dart'; 2 | 3 | int lcm(int a, int b) => (a * b) ~/ gcd(a, b); 4 | 5 | int gcd(int a, int b) { 6 | while (b != 0) { 7 | var t = b; 8 | b = a % t; 9 | a = t; 10 | } 11 | return a; 12 | } 13 | 14 | ///get third angle or side length in a triangle 15 | double getThirdSideLength(double a, double b, double angle) { 16 | double c2 = a * a + b * b - 2 * a * b * cos(angle); 17 | return sqrt(c2); 18 | } 19 | 20 | double getThirdAngle(double a, double b, double c) { 21 | double cosA = (a * a + b * b - c * c) / (2 * a * b); 22 | return acos(cosA); 23 | } 24 | 25 | ///get point coordinate/first derivative at parameter t on an arc 26 | Offset getPointOnArc(Rect rect, double t) { 27 | double xc = rect.center.dx, 28 | yc = rect.center.dy, 29 | rx = rect.width / 2, 30 | ry = rect.height / 2; 31 | return Offset(xc + rx * cos(t), yc + ry * sin(t)); 32 | } 33 | 34 | Offset getDerivativeOnArc(Rect rect, double t) { 35 | double rx = rect.width / 2, ry = rect.height / 2; 36 | return Offset(-rx * sin(t), ry * cos(t)); 37 | } 38 | 39 | ///recursively split an arc into multiple cubic Bezier 40 | List arcToCubicBezier(Rect rect, double startAngle, double sweepAngle, 41 | {double limit = pi / 4, int? splitTimes}) { 42 | if (splitTimes != null) { 43 | limit = sweepAngle.abs() / pow(2, splitTimes); 44 | } 45 | if (sweepAngle.abs() > limit) { 46 | List rst = 47 | arcToCubicBezier(rect, startAngle, sweepAngle / 2.0, limit: limit); 48 | rst 49 | ..addAll(arcToCubicBezier( 50 | rect, startAngle + sweepAngle / 2.0, sweepAngle / 2.0, 51 | limit: limit)); 52 | return rst; 53 | } 54 | 55 | double alpha = sin(sweepAngle) * 56 | (sqrt(4.0 + 3.0 * tan(sweepAngle / 2.0) * tan(sweepAngle / 2.0)) - 1.0) / 57 | 3.0; 58 | 59 | List rst = []; 60 | Offset p1, p2, p3, p4; 61 | p1 = getPointOnArc(rect, startAngle); 62 | p4 = getPointOnArc(rect, startAngle + sweepAngle); 63 | p2 = p1 + getDerivativeOnArc(rect, startAngle) * alpha; 64 | p3 = p4 - getDerivativeOnArc(rect, startAngle + sweepAngle) * alpha; 65 | rst.add(p1); 66 | rst.add(p2); 67 | rst.add(p3); 68 | rst.add(p4); 69 | 70 | return rst; 71 | } 72 | 73 | List rotateList(List list, int v) { 74 | if (list.isEmpty) return list; 75 | var i = v % list.length; 76 | return list.sublist(i)..addAll(list.sublist(0, i)); 77 | } 78 | -------------------------------------------------------------------------------- /pubspec.lock: -------------------------------------------------------------------------------- 1 | # Generated by pub 2 | # See https://dart.dev/tools/pub/glossary#lockfile 3 | packages: 4 | animated_box_decoration: 5 | dependency: "direct main" 6 | description: 7 | name: animated_box_decoration 8 | sha256: "4f36e77ed62a5420e9d0b5ee6c84b15fb210ed61154e6d0a0a5c7404b332e543" 9 | url: "https://pub.dev" 10 | source: hosted 11 | version: "0.0.7" 12 | async: 13 | dependency: transitive 14 | description: 15 | name: async 16 | sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" 17 | url: "https://pub.dev" 18 | source: hosted 19 | version: "2.11.0" 20 | boolean_selector: 21 | dependency: transitive 22 | description: 23 | name: boolean_selector 24 | sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" 25 | url: "https://pub.dev" 26 | source: hosted 27 | version: "2.1.1" 28 | characters: 29 | dependency: transitive 30 | description: 31 | name: characters 32 | sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" 33 | url: "https://pub.dev" 34 | source: hosted 35 | version: "1.3.0" 36 | clock: 37 | dependency: transitive 38 | description: 39 | name: clock 40 | sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf 41 | url: "https://pub.dev" 42 | source: hosted 43 | version: "1.1.1" 44 | collection: 45 | dependency: transitive 46 | description: 47 | name: collection 48 | sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf 49 | url: "https://pub.dev" 50 | source: hosted 51 | version: "1.19.0" 52 | dimension: 53 | dependency: "direct main" 54 | description: 55 | name: dimension 56 | sha256: "50d0df1353a5a16c608a888549976765e34ccb216f1e4fe5415196d6b3d0c5b6" 57 | url: "https://pub.dev" 58 | source: hosted 59 | version: "0.1.7" 60 | fake_async: 61 | dependency: transitive 62 | description: 63 | name: fake_async 64 | sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" 65 | url: "https://pub.dev" 66 | source: hosted 67 | version: "1.3.1" 68 | flutter: 69 | dependency: "direct main" 70 | description: flutter 71 | source: sdk 72 | version: "0.0.0" 73 | flutter_class_parser: 74 | dependency: "direct main" 75 | description: 76 | name: flutter_class_parser 77 | sha256: d975dd35ec7a93fd6099309496d423211145039a97d09d192cae013ff6fb10d9 78 | url: "https://pub.dev" 79 | source: hosted 80 | version: "0.2.5" 81 | flutter_test: 82 | dependency: "direct dev" 83 | description: flutter 84 | source: sdk 85 | version: "0.0.0" 86 | leak_tracker: 87 | dependency: transitive 88 | description: 89 | name: leak_tracker 90 | sha256: "7bb2830ebd849694d1ec25bf1f44582d6ac531a57a365a803a6034ff751d2d06" 91 | url: "https://pub.dev" 92 | source: hosted 93 | version: "10.0.7" 94 | leak_tracker_flutter_testing: 95 | dependency: transitive 96 | description: 97 | name: leak_tracker_flutter_testing 98 | sha256: "9491a714cca3667b60b5c420da8217e6de0d1ba7a5ec322fab01758f6998f379" 99 | url: "https://pub.dev" 100 | source: hosted 101 | version: "3.0.8" 102 | leak_tracker_testing: 103 | dependency: transitive 104 | description: 105 | name: leak_tracker_testing 106 | sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" 107 | url: "https://pub.dev" 108 | source: hosted 109 | version: "3.0.1" 110 | matcher: 111 | dependency: transitive 112 | description: 113 | name: matcher 114 | sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb 115 | url: "https://pub.dev" 116 | source: hosted 117 | version: "0.12.16+1" 118 | material_color_utilities: 119 | dependency: transitive 120 | description: 121 | name: material_color_utilities 122 | sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec 123 | url: "https://pub.dev" 124 | source: hosted 125 | version: "0.11.1" 126 | meta: 127 | dependency: transitive 128 | description: 129 | name: meta 130 | sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 131 | url: "https://pub.dev" 132 | source: hosted 133 | version: "1.15.0" 134 | path: 135 | dependency: transitive 136 | description: 137 | name: path 138 | sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" 139 | url: "https://pub.dev" 140 | source: hosted 141 | version: "1.9.0" 142 | sky_engine: 143 | dependency: transitive 144 | description: flutter 145 | source: sdk 146 | version: "0.0.0" 147 | source_span: 148 | dependency: transitive 149 | description: 150 | name: source_span 151 | sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" 152 | url: "https://pub.dev" 153 | source: hosted 154 | version: "1.10.0" 155 | stack_trace: 156 | dependency: transitive 157 | description: 158 | name: stack_trace 159 | sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377" 160 | url: "https://pub.dev" 161 | source: hosted 162 | version: "1.12.0" 163 | stream_channel: 164 | dependency: transitive 165 | description: 166 | name: stream_channel 167 | sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 168 | url: "https://pub.dev" 169 | source: hosted 170 | version: "2.1.2" 171 | string_scanner: 172 | dependency: transitive 173 | description: 174 | name: string_scanner 175 | sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3" 176 | url: "https://pub.dev" 177 | source: hosted 178 | version: "1.3.0" 179 | term_glyph: 180 | dependency: transitive 181 | description: 182 | name: term_glyph 183 | sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 184 | url: "https://pub.dev" 185 | source: hosted 186 | version: "1.2.1" 187 | test_api: 188 | dependency: transitive 189 | description: 190 | name: test_api 191 | sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c" 192 | url: "https://pub.dev" 193 | source: hosted 194 | version: "0.7.3" 195 | vector_math: 196 | dependency: transitive 197 | description: 198 | name: vector_math 199 | sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" 200 | url: "https://pub.dev" 201 | source: hosted 202 | version: "2.1.4" 203 | vm_service: 204 | dependency: transitive 205 | description: 206 | name: vm_service 207 | sha256: f6be3ed8bd01289b34d679c2b62226f63c0e69f9fd2e50a6b3c1c729a961041b 208 | url: "https://pub.dev" 209 | source: hosted 210 | version: "14.3.0" 211 | sdks: 212 | dart: ">=3.4.0 <4.0.0" 213 | flutter: ">=3.18.0-18.0.pre.54" 214 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: morphable_shape 2 | description: A Flutter package for creating various shapes that are responsive and can morph betweem each other. 3 | version: 2.0.0 4 | repository: https://github.com/KevinVan720/morphable_shape 5 | 6 | environment: 7 | sdk: ">=2.14.0 <4.0.0" 8 | flutter: ">=2.10.0" 9 | 10 | dependencies: 11 | flutter: 12 | sdk: flutter 13 | 14 | flutter_class_parser: ^0.2.5 15 | dimension: ^0.2.0 16 | animated_box_decoration: 0.0.7 17 | 18 | dev_dependencies: 19 | flutter_test: 20 | sdk: flutter 21 | 22 | # The following section is specific to Flutter. 23 | flutter: 24 | 25 | -------------------------------------------------------------------------------- /test/morphable_shape_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_test/flutter_test.dart'; 2 | 3 | void main() { 4 | test('no test at present', () { 5 | }); 6 | } 7 | --------------------------------------------------------------------------------