├── .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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
209 |
210 | 
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 | 
274 | 
275 | 
276 | 
277 | 
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 |
--------------------------------------------------------------------------------