├── .gitignore ├── .metadata ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── analysis_options.yaml ├── example ├── .gitignore ├── .metadata ├── README.md ├── analysis_options.yaml ├── lib │ └── main.dart └── pubspec.yaml ├── golden ├── 0.png ├── 1.png ├── 10.png ├── 11.png ├── 12.png ├── 13.png ├── 14.png ├── 15.png ├── 16.png ├── 17.png ├── 18.png ├── 19.png ├── 2.png ├── 20.png ├── 21.png ├── 22.png ├── 23.png ├── 24.png ├── 25.png ├── 26.png ├── 27.png ├── 28.png ├── 29.png ├── 3.png ├── 30.png ├── 31.png ├── 32.png ├── 33.png ├── 34.png ├── 35.png ├── 36.png ├── 37.png ├── 38.png ├── 39.png ├── 4.png ├── 40.png ├── 41.png ├── 42.png ├── 43.png ├── 5.png ├── 6.png ├── 7.png ├── 8.png └── 9.png ├── lib ├── path_drawing.dart └── src │ ├── dash_path.dart │ ├── parse_path.dart │ └── trim_path.dart ├── path_drawing.iml ├── path_drawing_android.iml ├── pubspec.yaml ├── test ├── dash_path_test.dart ├── render_path_test.dart └── trim_path_test.dart └── tool └── path_to_image.dart /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .dart_tool/ 3 | 4 | .packages 5 | .pub/ 6 | .idea/ 7 | 8 | build/ 9 | coverage/ 10 | 11 | .flutter-plugins 12 | 13 | pubspec.lock 14 | -------------------------------------------------------------------------------- /.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: 1d915bacc0077bdd38869d480f12c71377b43157 8 | channel: master 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | os: 2 | - linux 3 | sudo: false 4 | addons: 5 | apt: 6 | # Flutter depends on /usr/lib/x86_64-linux-gnu/libstdc++.so.6 version GLIBCXX_3.4.18 7 | sources: 8 | - ubuntu-toolchain-r-test # if we don't specify this, the libstdc++6 we get is the wrong version 9 | packages: 10 | - libstdc++6 11 | - fonts-droid 12 | before_script: 13 | - git clone https://github.com/flutter/flutter.git -b dev --single-branch 14 | - ./flutter/bin/flutter doctor 15 | - gem install coveralls-lcov 16 | script: 17 | - ./flutter/bin/flutter analyze 18 | - ./flutter/bin/flutter test --coverage 19 | after_success: 20 | - coveralls-lcov coverage/lcov.info 21 | cache: 22 | directories: 23 | - $HOME/.pub-cache 24 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | ## 1.0.1 4 | 5 | - Bump path parsing dependency to fix bugs in path parsing. Update goldens. 6 | - Analysis cleanup. 7 | - Remove unnecessary platform folders from example. 8 | 9 | ## 1.0.0 10 | 11 | - Stable release. 12 | 13 | ## 0.5.1+1 14 | 15 | - Make `DashOffset` comparable. 16 | 17 | ## 0.5.1 18 | 19 | - Bump path_parsing dependency. 20 | 21 | ## 0.5.0 22 | 23 | - Stable nullsafe release. 24 | 25 | ## 0.5.0-nullsafety.0 26 | 27 | - Null safe migration, general modernization 28 | 29 | ## 0.4.1+1 30 | 31 | - Set uses-material-design to false. 32 | 33 | ## 0.4.1 34 | 35 | - Consume latest path_parsing version. 36 | 37 | ## 0.4.0 38 | 39 | - Implement path trimming routine 40 | - Update example to demonstrate dash paths and trim paths. 41 | - Remove `new` keyword. 42 | 43 | ## 0.3.1 44 | 45 | - Consume updated version of parsing library 46 | - Fix layout to conform to newer Flutter package requirements. 47 | 48 | ## 0.3.0 49 | 50 | - Split out parsing logic into separate package that does not depend on Flutter 51 | - Consume latest version of package, which fixes bug in smooth curve parsing 52 | 53 | ## 0.2.x 54 | 55 | - 0.2.4: Fix bug in exponent validation logic 56 | - 0.2.3: Fix bugs in _decomposeCubic - `Offset.scale` and `Offset.translate` related 57 | - 0.2.2: Fix handling of null dashOffset 58 | - 0.2.1: Add support for dashed paths, update docs 59 | - 0.2.0: _not published_ 60 | 61 | ## 0.1.x 62 | 63 | - 0.1.1: Fix bug in matrix translate logic 64 | - 0.1.0: Initial release 65 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018 Dan Field 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # path_drawing 2 | 3 | [![pub package](https://img.shields.io/pub/v/path_drawing.svg)](https://pub.dev/packages/path_drawing) 4 | 5 | A Flutter library to assist with creating and manipulating paths. 6 | 7 | Currently supports parsing a `Path` from an SVG path data string 8 | (including normalizing the path commands to be amenable to Flutter's exposed 9 | Path methods). 10 | 11 | Dash paths has an initial implementation that relies on flutter 0.3.6 at a minimum. 12 | 13 | Planned for future release(s): 14 | 15 | - Trim paths 16 | 17 | ## Example 18 | 19 | Parse some path from svg string: 20 | 21 | ```dart 22 | import 'package:path_drawing/path_drawing.dart'; 23 | 24 | final trianglePath = parseSvgPathData('M150 0 L75 200 L225 200 Z'); 25 | ``` 26 | 27 | Create [CustomPainter](https://api.flutter.dev/flutter/rendering/CustomPainter-class.html): 28 | 29 | ```dart 30 | class FilledPathPainter extends CustomPainter { 31 | const FilledPathPainter({ 32 | @required this.path, 33 | @required this.color, 34 | }); 35 | 36 | final Path path; 37 | final Color color; 38 | 39 | @override 40 | bool shouldRepaint(FilledPathPainter oldDelegate) => 41 | oldDelegate.path != path || oldDelegate.color != color; 42 | 43 | @override 44 | void paint(Canvas canvas, Size size) { 45 | canvas.drawPath( 46 | path, 47 | Paint() 48 | ..color = color 49 | ..style = PaintingStyle.fill, 50 | ); 51 | } 52 | 53 | @override 54 | bool hitTest(Offset position) => path.contains(position); 55 | } 56 | ``` 57 | 58 | Use it inside [CustomPaint](https://api.flutter.dev/flutter/widgets/CustomPaint-class.html): 59 | 60 | ```dart 61 | class MyWidget extends StatelessWidget { 62 | @override 63 | Widget build(BuildContext context) { 64 | return GestureDetector( 65 | onTap: () => print('tap'), 66 | child: CustomPaint( 67 | painter: FilledPathPainter( 68 | path: trianglePath, 69 | color: Colors.blue, 70 | ), 71 | ), 72 | ); 73 | } 74 | } 75 | ``` 76 | 77 | More examples can be found in [example folder](example/lib/main.dart) 78 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # Specify analysis options. 2 | # 3 | # Until there are meta linter rules, each desired lint must be explicitly enabled. 4 | # See: https://github.com/dart-lang/linter/issues/288 5 | # 6 | # For a list of lints, see: http://dart-lang.github.io/linter/lints/ 7 | # See the configuration guide for more 8 | # https://github.com/dart-lang/sdk/tree/master/pkg/analyzer#configuring-the-analyzer 9 | # 10 | # There are four similar analysis options files in the flutter repos: 11 | # - analysis_options.yaml (this file) 12 | # - packages/flutter/lib/analysis_options_user.yaml 13 | # - https://github.com/flutter/plugins/blob/master/analysis_options.yaml 14 | # - https://github.com/flutter/engine/blob/master/analysis_options.yaml 15 | # 16 | # This file contains the analysis options used by Flutter tools, such as IntelliJ, 17 | # Android Studio, and the `flutter analyze` command. 18 | # 19 | # The flutter/plugins repo contains a copy of this file, which should be kept 20 | # in sync with this file. 21 | 22 | analyzer: 23 | strong-mode: 24 | implicit-dynamic: false 25 | errors: 26 | # treat missing required parameters as a warning (not a hint) 27 | missing_required_param: warning 28 | # treat missing returns as a warning (not a hint) 29 | missing_return: warning 30 | # allow having TODOs in the code 31 | todo: ignore 32 | exclude: 33 | # for Travis - don't try to analyze the flutter repo 34 | - 'flutter/**' 35 | 36 | 37 | linter: 38 | rules: 39 | # these rules are documented on and in the same order as 40 | # the Dart Lint rules page to make maintenance easier 41 | # https://github.com/dart-lang/linter/blob/master/example/all.yaml 42 | - always_declare_return_types 43 | - always_put_control_body_on_new_line 44 | # - always_put_required_named_parameters_first # we prefer having parameters in the same order as fields https://github.com/flutter/flutter/issues/10219 45 | - always_require_non_null_named_parameters 46 | - always_specify_types 47 | - annotate_overrides 48 | # - avoid_annotating_with_dynamic # conflicts with always_specify_types 49 | # - avoid_bool_literals_in_conditional_expressions # not yet tested 50 | # - avoid_catches_without_on_clauses # we do this commonly 51 | # - avoid_catching_errors # we do this commonly 52 | - avoid_classes_with_only_static_members 53 | - avoid_empty_else 54 | - avoid_function_literals_in_foreach_calls 55 | - avoid_init_to_null 56 | - avoid_null_checks_in_equality_operators 57 | # - avoid_positional_boolean_parameters # not yet tested 58 | # - avoid_private_typedef_functions # we prefer having typedef (discussion in https://github.com/flutter/flutter/pull/16356) 59 | - avoid_relative_lib_imports 60 | - avoid_renaming_method_parameters 61 | - avoid_return_types_on_setters 62 | # - avoid_returning_null # we do this commonly 63 | # - avoid_returning_this # https://github.com/dart-lang/linter/issues/842 64 | # - avoid_setters_without_getters # not yet tested 65 | # - avoid_single_cascade_in_expression_statements # not yet tested 66 | - avoid_slow_async_io 67 | # - avoid_types_as_parameter_names # https://github.com/dart-lang/linter/pull/954/files 68 | # - avoid_types_on_closure_parameters # conflicts with always_specify_types 69 | # - avoid_unused_constructor_parameters # https://github.com/dart-lang/linter/pull/847 70 | - await_only_futures 71 | - camel_case_types 72 | - cancel_subscriptions 73 | # - cascade_invocations # not yet tested 74 | # - close_sinks # https://github.com/flutter/flutter/issues/5789 75 | # - comment_references # blocked on https://github.com/dart-lang/dartdoc/issues/1153 76 | # - constant_identifier_names # https://github.com/dart-lang/linter/issues/204 77 | - control_flow_in_finally 78 | - directives_ordering 79 | - empty_catches 80 | - empty_constructor_bodies 81 | - empty_statements 82 | - hash_and_equals 83 | - implementation_imports 84 | # - invariant_booleans # https://github.com/flutter/flutter/issues/5790 85 | - iterable_contains_unrelated_type 86 | # - join_return_with_assignment # not yet tested 87 | - library_names 88 | - library_prefixes 89 | - list_remove_unrelated_type 90 | # - literal_only_boolean_expressions # https://github.com/flutter/flutter/issues/5791 91 | - no_adjacent_strings_in_list 92 | - no_duplicate_case_values 93 | - non_constant_identifier_names 94 | # - omit_local_variable_types # opposite of always_specify_types 95 | # - one_member_abstracts # too many false positives 96 | # - only_throw_errors # https://github.com/flutter/flutter/issues/5792 97 | - overridden_fields 98 | - package_api_docs 99 | - package_names 100 | - package_prefixed_library_names 101 | # - parameter_assignments # we do this commonly 102 | - prefer_adjacent_string_concatenation 103 | - prefer_asserts_in_initializer_lists 104 | - prefer_collection_literals 105 | - prefer_conditional_assignment 106 | - prefer_const_constructors 107 | - prefer_const_constructors_in_immutables 108 | - prefer_const_declarations 109 | - prefer_const_literals_to_create_immutables 110 | # - prefer_constructors_over_static_methods # not yet tested 111 | - prefer_contains 112 | # - prefer_equal_for_default_values # not yet tested 113 | # - prefer_expression_function_bodies # conflicts with https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo#consider-using--for-short-functions-and-methods 114 | - prefer_final_fields 115 | - prefer_final_locals 116 | - prefer_foreach 117 | # - prefer_function_declarations_over_variables # not yet tested 118 | - prefer_initializing_formals 119 | # - prefer_interpolation_to_compose_strings # not yet tested 120 | - prefer_is_empty 121 | - prefer_is_not_empty 122 | - prefer_single_quotes 123 | - prefer_typing_uninitialized_variables 124 | - recursive_getters 125 | - slash_for_doc_comments 126 | - sort_constructors_first 127 | - sort_unnamed_constructors_first 128 | - test_types_in_equals 129 | - throw_in_finally 130 | # - type_annotate_public_apis # subset of always_specify_types 131 | - type_init_formals 132 | # - unawaited_futures # https://github.com/flutter/flutter/issues/5793 133 | - unnecessary_brace_in_string_interps 134 | - unnecessary_getters_setters 135 | # - unnecessary_lambdas # https://github.com/dart-lang/linter/issues/498 136 | - unnecessary_null_aware_assignments 137 | - unnecessary_null_in_if_null_operators 138 | - unnecessary_overrides 139 | - unnecessary_parenthesis 140 | # - unnecessary_statements # not yet tested 141 | - unnecessary_this 142 | - unrelated_type_equality_checks 143 | - use_rethrow_when_possible 144 | # - use_setters_to_change_properties # not yet tested 145 | # - use_string_buffers # https://github.com/dart-lang/linter/pull/664 146 | # - use_to_and_as_if_applicable # has false positives, so we prefer to catch this by code-review 147 | - valid_regexps 148 | - unnecessary_new 149 | -------------------------------------------------------------------------------- /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 | ios/ 32 | android/ 33 | linux/ 34 | macos/ 35 | web/ 36 | windows/ 37 | 38 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages 39 | -------------------------------------------------------------------------------- /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. 5 | 6 | version: 7 | revision: 8e77cb4341d8635a0ead34bf17ca80fb74d5ead9 8 | channel: master 9 | 10 | project_type: app 11 | 12 | # Tracks metadata for the flutter migrate command 13 | migration: 14 | platforms: 15 | - platform: root 16 | create_revision: 8e77cb4341d8635a0ead34bf17ca80fb74d5ead9 17 | base_revision: 8e77cb4341d8635a0ead34bf17ca80fb74d5ead9 18 | - platform: android 19 | create_revision: 8e77cb4341d8635a0ead34bf17ca80fb74d5ead9 20 | base_revision: 8e77cb4341d8635a0ead34bf17ca80fb74d5ead9 21 | - platform: ios 22 | create_revision: 8e77cb4341d8635a0ead34bf17ca80fb74d5ead9 23 | base_revision: 8e77cb4341d8635a0ead34bf17ca80fb74d5ead9 24 | - platform: linux 25 | create_revision: 8e77cb4341d8635a0ead34bf17ca80fb74d5ead9 26 | base_revision: 8e77cb4341d8635a0ead34bf17ca80fb74d5ead9 27 | - platform: macos 28 | create_revision: 8e77cb4341d8635a0ead34bf17ca80fb74d5ead9 29 | base_revision: 8e77cb4341d8635a0ead34bf17ca80fb74d5ead9 30 | - platform: web 31 | create_revision: 8e77cb4341d8635a0ead34bf17ca80fb74d5ead9 32 | base_revision: 8e77cb4341d8635a0ead34bf17ca80fb74d5ead9 33 | - platform: windows 34 | create_revision: 8e77cb4341d8635a0ead34bf17ca80fb74d5ead9 35 | base_revision: 8e77cb4341d8635a0ead34bf17ca80fb74d5ead9 36 | 37 | # User provided section 38 | 39 | # List of Local paths (relative to this file) that should be 40 | # ignored by the migrate tool. 41 | # 42 | # Files that are not part of the templates will be ignored by default. 43 | unmanaged_files: 44 | - 'lib/main.dart' 45 | - 'ios/Runner.xcodeproj/project.pbxproj' 46 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # example 2 | 3 | A new Flutter project. 4 | 5 | ## Getting Started 6 | 7 | For help getting started with Flutter, view our online 8 | [documentation](https://flutter.io/). 9 | -------------------------------------------------------------------------------- /example/analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # This file configures the analyzer, which statically analyzes Dart code to 2 | # check for errors, warnings, and lints. 3 | # 4 | # The issues identified by the analyzer are surfaced in the UI of Dart-enabled 5 | # IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be 6 | # invoked from the command line by running `flutter analyze`. 7 | 8 | # The following line activates a set of recommended lints for Flutter apps, 9 | # packages, and plugins designed to encourage good coding practices. 10 | include: package:flutter_lints/flutter.yaml 11 | 12 | linter: 13 | # The lint rules applied to this project can be customized in the 14 | # section below to disable rules from the `package:flutter_lints/flutter.yaml` 15 | # included above or to enable additional rules. A list of all available lints 16 | # and their documentation is published at 17 | # https://dart-lang.github.io/linter/lints/index.html. 18 | # 19 | # Instead of disabling a lint rule for the entire project in the 20 | # section below, it can also be suppressed for a single line of code 21 | # or a specific dart file by using the `// ignore: name_of_lint` and 22 | # `// ignore_for_file: name_of_lint` syntax on the line or in the file 23 | # producing the lint. 24 | rules: 25 | # avoid_print: false # Uncomment to disable the `avoid_print` rule 26 | # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule 27 | 28 | # Additional information about this file can be found at 29 | # https://dart.dev/guides/language/analysis-options 30 | -------------------------------------------------------------------------------- /example/lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:path_drawing/path_drawing.dart'; 3 | 4 | void main() => runApp(const MyApp()); 5 | 6 | class MyApp extends StatelessWidget { 7 | const MyApp({Key? key}) : super(key: key); 8 | 9 | @override 10 | Widget build(BuildContext context) { 11 | return MaterialApp( 12 | title: 'Flutter Demo', 13 | theme: ThemeData( 14 | primarySwatch: Colors.blue, 15 | ), 16 | home: const MyHomePage(title: 'Flutter Demo Home Page'), 17 | ); 18 | } 19 | } 20 | 21 | class MyHomePage extends StatefulWidget { 22 | const MyHomePage({Key? key, required this.title}) : super(key: key); 23 | 24 | final String title; 25 | 26 | @override 27 | State createState() => _MyHomePageState(); 28 | } 29 | 30 | class _MyHomePageState extends State { 31 | late int index; 32 | late double _trimPercent; 33 | late PathTrimOrigin _trimOrigin; 34 | 35 | @override 36 | void initState() { 37 | super.initState(); 38 | index = 0; 39 | _trimPercent = 0.2; 40 | _trimOrigin = PathTrimOrigin.begin; 41 | } 42 | 43 | String get currPath => paths[index]; 44 | 45 | void nextPath() { 46 | setState(() => index = index >= paths.length - 1 ? 0 : index + 1); 47 | } 48 | 49 | void prevPath() { 50 | setState(() => index = index == 0 ? paths.length - 1 : index - 1); 51 | } 52 | 53 | void setTrimPercent(double value) { 54 | setState(() { 55 | _trimPercent = value; 56 | }); 57 | } 58 | 59 | void toggleTrimOrigin(PathTrimOrigin? value) { 60 | setState(() { 61 | switch (_trimOrigin) { 62 | case PathTrimOrigin.begin: 63 | _trimOrigin = PathTrimOrigin.end; 64 | break; 65 | case PathTrimOrigin.end: 66 | _trimOrigin = PathTrimOrigin.begin; 67 | break; 68 | } 69 | }); 70 | } 71 | 72 | @override 73 | Widget build(BuildContext context) { 74 | return DefaultTabController( 75 | length: 3, 76 | child: Scaffold( 77 | appBar: AppBar( 78 | title: Text(widget.title), 79 | bottom: const TabBar( 80 | tabs: [ 81 | Tab(text: 'Path Trim'), 82 | Tab(text: 'Path Dash'), 83 | Tab(text: 'Path Parse'), 84 | ], 85 | ), 86 | ), 87 | body: TabBarView( 88 | children: [ 89 | Stack( 90 | children: [ 91 | CustomPaint( 92 | painter: TrimPathPainter(_trimPercent, _trimOrigin)), 93 | Align( 94 | alignment: Alignment.bottomCenter, 95 | child: Column( 96 | mainAxisAlignment: MainAxisAlignment.end, 97 | children: [ 98 | Slider( 99 | value: _trimPercent, 100 | onChanged: (double value) => setTrimPercent(value), 101 | ), 102 | RadioListTile( 103 | title: Text(PathTrimOrigin.begin.toString()), 104 | value: PathTrimOrigin.begin, 105 | groupValue: _trimOrigin, 106 | onChanged: toggleTrimOrigin, 107 | ), 108 | RadioListTile( 109 | title: Text(PathTrimOrigin.end.toString()), 110 | value: PathTrimOrigin.end, 111 | groupValue: _trimOrigin, 112 | onChanged: toggleTrimOrigin, 113 | ), 114 | ], 115 | ), 116 | ), 117 | ], 118 | ), 119 | CustomPaint(painter: DashPathPainter()), 120 | Stack( 121 | children: [ 122 | CustomPaint(painter: PathTestPainter(currPath)), 123 | GestureDetector( 124 | onTap: nextPath, 125 | ), 126 | ], 127 | ), 128 | ], 129 | ), 130 | ), 131 | ); 132 | } 133 | } 134 | 135 | const List paths = [ 136 | 'm18 11.8a.41.41 0 0 1 .24.08l.59.43h.05.72a.4.4 0 0 1 .39.28l.22.69a.08.08 0 0 0 0 0l.58.43a.41.41 0 0 1 .15.45l-.22.68a.09.09 0 0 0 0 .07l.22.68a.4.4 0 0 1 -.15.46l-.58.42a.1.1 0 0 0 0 0l-.22.68a.41.41 0 0 1 -.38.29h-.79l-.58.43a.41.41 0 0 1 -.24.08.46.46 0 0 1 -.24-.08l-.58-.43h-.06-.72a.41.41 0 0 1 -.39-.28l-.22-.68a.1.1 0 0 0 0 0l-.58-.43a.42.42 0 0 1 -.15-.46l.23-.67v-.02l-.29-.68a.43.43 0 0 1 .15-.46l.58-.42a.1.1 0 0 0 0-.05l.27-.69a.42.42 0 0 1 .39-.28h.78l.58-.43a.43.43 0 0 1 .25-.09m0-1a1.37 1.37 0 0 0 -.83.27l-.34.25h-.43a1.42 1.42 0 0 0 -1.34 1l-.13.4-.35.25a1.42 1.42 0 0 0 -.51 1.58l.13.4-.13.4a1.39 1.39 0 0 0 .52 1.59l.34.25.13.4a1.41 1.41 0 0 0 1.34 1h.43l.34.26a1.44 1.44 0 0 0 .83.27 1.38 1.38 0 0 0 .83-.28l.35-.24h.43a1.4 1.4 0 0 0 1.33-1l.13-.4.35-.26a1.39 1.39 0 0 0 .51-1.57l-.13-.4.13-.41a1.4 1.4 0 0 0 -.51-1.56l-.35-.25-.13-.41a1.4 1.4 0 0 0 -1.34-1h-.42l-.34-.26a1.43 1.43 0 0 0 -.84-.28z', 137 | '''M 15 15.5 A 0.5 1.5 0 1 1 14,15.5 A 0.5 1.5 0 1 1 15 15.5 z''', 138 | 'M100,200 L3,4', 139 | 'M100,200 l3,4', 140 | 'M100,200 H3', 141 | 'M100,200 h3', 142 | 'M100,200 V3', 143 | 'M100,200 v3', 144 | 'M100,200 C3,4,5,6,7,8', 145 | 'M100,200 c3,4,5,6,7,8', 146 | 'M100,200 S3,4,5,6', 147 | 'M100,200 s3,4,5,6', 148 | 'M100,200 Q3,4,5,6', 149 | 'M100,200 q3,4,5,6', 150 | 'M100,200 T3,4', 151 | 'M100,200 t3,4', 152 | 'M100,200 A3,4,5,0,0,6,7', 153 | 'M100,200 A3,4,5,1,0,6,7', 154 | 'M100,200 A3,4,5,0,1,6,7', 155 | 'M100,200 A3,4,5,1,1,6,7', 156 | 'M100,200 a3,4,5,0,0,6,7', 157 | 'M100,200 a3,4,5,0,1,6,7', 158 | 'M100,200 a3,4,5,1,0,6,7', 159 | 'M100,200 a3,4,5,1,1,6,7', 160 | 'M100,200 a3,4,5,006,7', 161 | 'M100,200 a3,4,5,016,7', 162 | 'M100,200 a3,4,5,106,7', 163 | 'M100,200 a3,4,5,116,7', 164 | '''M19.0281,19.40466 20.7195,19.40466 20.7195,15.71439 24.11486,15.71439 24.11486,14.36762 20.7195,14.36762 165 | 20.7195,11.68641 24.74134,11.68641 24.74134,10.34618 19.0281,10.34618 z''', 166 | 'M100,200 a0,4,5,0,0,10,0 a4,0,5,0,0,0,10 a0,0,5,0,0,-10,0 z', 167 | 'M.1 .2 L.3 .4 .5 .6', 168 | 'M1,1h2,3', 169 | 'M1,1H2,3', 170 | 'M1,1v2,3', 171 | 'M1,1V2,3', 172 | 'M1,1c2,3 4,5 6,7 8,9 10,11 12,13', 173 | 'M1,1C2,3 4,5 6,7 8,9 10,11 12,13', 174 | 'M1,1s2,3 4,5 6,7 8,9', 175 | 'M1,1S2,3 4,5 6,7 8,9', 176 | 'M1,1q2,3 4,5 6,7 8,9', 177 | 'M1,1Q2,3 4,5 6,7 8,9', 178 | 'M1,1t2,3 4,5', 179 | 'M1,1T2,3 4,5', 180 | 'M1,1a2,3,4,0,0,5,6 7,8,9,0,0,10,11', 181 | 'M1,1A2,3,4,0,0,5,6 7,8,9,0,0,10,11', 182 | ]; 183 | 184 | final Paint black = Paint() 185 | ..color = Colors.black 186 | ..strokeWidth = 1.0 187 | ..style = PaintingStyle.stroke; 188 | 189 | class TrimPathPainter extends CustomPainter { 190 | TrimPathPainter(this.percent, this.origin); 191 | 192 | final double percent; 193 | final PathTrimOrigin origin; 194 | 195 | final Path p = Path() 196 | ..moveTo(10.0, 10.0) 197 | ..lineTo(100.0, 100.0) 198 | ..quadraticBezierTo(125.0, 20.0, 200.0, 100.0); 199 | 200 | @override 201 | bool shouldRepaint(TrimPathPainter oldDelegate) => 202 | oldDelegate.percent != percent; 203 | 204 | @override 205 | void paint(Canvas canvas, Size size) { 206 | canvas.drawPath(trimPath(p, percent, origin: origin), black); 207 | } 208 | } 209 | 210 | class DashPathPainter extends CustomPainter { 211 | final Path p = Path() 212 | ..moveTo(10.0, 10.0) 213 | ..lineTo(100.0, 100.0) 214 | ..quadraticBezierTo(125.0, 20.0, 200.0, 100.0) 215 | ..addRect(const Rect.fromLTWH(0.0, 0.0, 50.0, 50.0)); 216 | 217 | @override 218 | bool shouldRepaint(DashPathPainter oldDelegate) => true; 219 | 220 | @override 221 | void paint(Canvas canvas, Size size) { 222 | canvas.drawPath( 223 | dashPath( 224 | p, 225 | dashArray: CircularIntervalList( 226 | [5.0, 2.5], 227 | ), 228 | ), 229 | black); 230 | } 231 | } 232 | 233 | class PathTestPainter extends CustomPainter { 234 | PathTestPainter(String path) : p = parseSvgPathData(path); 235 | 236 | final Path p; 237 | 238 | @override 239 | bool shouldRepaint(PathTestPainter oldDelegate) => true; 240 | 241 | @override 242 | void paint(Canvas canvas, Size size) { 243 | canvas.drawPath(p, black); 244 | } 245 | } 246 | -------------------------------------------------------------------------------- /example/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: example 2 | description: A new Flutter project. 3 | publish_to: none 4 | 5 | environment: 6 | sdk: '>=2.12.0-0 <3.0.0' 7 | flutter: '>=1.24.0-7.0 <2.0.0' 8 | 9 | dependencies: 10 | path_drawing: 11 | path: ../ 12 | flutter: 13 | sdk: flutter 14 | 15 | dev_dependencies: 16 | flutter_test: 17 | sdk: flutter 18 | flutter_lints: ^2.0.1 19 | 20 | flutter: 21 | uses-material-design: true 22 | -------------------------------------------------------------------------------- /golden/0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnfield/flutter_path_drawing/27ce967273429ce505962d4cf68c17a5b16b5aef/golden/0.png -------------------------------------------------------------------------------- /golden/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnfield/flutter_path_drawing/27ce967273429ce505962d4cf68c17a5b16b5aef/golden/1.png -------------------------------------------------------------------------------- /golden/10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnfield/flutter_path_drawing/27ce967273429ce505962d4cf68c17a5b16b5aef/golden/10.png -------------------------------------------------------------------------------- /golden/11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnfield/flutter_path_drawing/27ce967273429ce505962d4cf68c17a5b16b5aef/golden/11.png -------------------------------------------------------------------------------- /golden/12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnfield/flutter_path_drawing/27ce967273429ce505962d4cf68c17a5b16b5aef/golden/12.png -------------------------------------------------------------------------------- /golden/13.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnfield/flutter_path_drawing/27ce967273429ce505962d4cf68c17a5b16b5aef/golden/13.png -------------------------------------------------------------------------------- /golden/14.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnfield/flutter_path_drawing/27ce967273429ce505962d4cf68c17a5b16b5aef/golden/14.png -------------------------------------------------------------------------------- /golden/15.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnfield/flutter_path_drawing/27ce967273429ce505962d4cf68c17a5b16b5aef/golden/15.png -------------------------------------------------------------------------------- /golden/16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnfield/flutter_path_drawing/27ce967273429ce505962d4cf68c17a5b16b5aef/golden/16.png -------------------------------------------------------------------------------- /golden/17.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnfield/flutter_path_drawing/27ce967273429ce505962d4cf68c17a5b16b5aef/golden/17.png -------------------------------------------------------------------------------- /golden/18.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnfield/flutter_path_drawing/27ce967273429ce505962d4cf68c17a5b16b5aef/golden/18.png -------------------------------------------------------------------------------- /golden/19.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnfield/flutter_path_drawing/27ce967273429ce505962d4cf68c17a5b16b5aef/golden/19.png -------------------------------------------------------------------------------- /golden/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnfield/flutter_path_drawing/27ce967273429ce505962d4cf68c17a5b16b5aef/golden/2.png -------------------------------------------------------------------------------- /golden/20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnfield/flutter_path_drawing/27ce967273429ce505962d4cf68c17a5b16b5aef/golden/20.png -------------------------------------------------------------------------------- /golden/21.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnfield/flutter_path_drawing/27ce967273429ce505962d4cf68c17a5b16b5aef/golden/21.png -------------------------------------------------------------------------------- /golden/22.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnfield/flutter_path_drawing/27ce967273429ce505962d4cf68c17a5b16b5aef/golden/22.png -------------------------------------------------------------------------------- /golden/23.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnfield/flutter_path_drawing/27ce967273429ce505962d4cf68c17a5b16b5aef/golden/23.png -------------------------------------------------------------------------------- /golden/24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnfield/flutter_path_drawing/27ce967273429ce505962d4cf68c17a5b16b5aef/golden/24.png -------------------------------------------------------------------------------- /golden/25.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnfield/flutter_path_drawing/27ce967273429ce505962d4cf68c17a5b16b5aef/golden/25.png -------------------------------------------------------------------------------- /golden/26.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnfield/flutter_path_drawing/27ce967273429ce505962d4cf68c17a5b16b5aef/golden/26.png -------------------------------------------------------------------------------- /golden/27.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnfield/flutter_path_drawing/27ce967273429ce505962d4cf68c17a5b16b5aef/golden/27.png -------------------------------------------------------------------------------- /golden/28.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnfield/flutter_path_drawing/27ce967273429ce505962d4cf68c17a5b16b5aef/golden/28.png -------------------------------------------------------------------------------- /golden/29.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnfield/flutter_path_drawing/27ce967273429ce505962d4cf68c17a5b16b5aef/golden/29.png -------------------------------------------------------------------------------- /golden/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnfield/flutter_path_drawing/27ce967273429ce505962d4cf68c17a5b16b5aef/golden/3.png -------------------------------------------------------------------------------- /golden/30.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnfield/flutter_path_drawing/27ce967273429ce505962d4cf68c17a5b16b5aef/golden/30.png -------------------------------------------------------------------------------- /golden/31.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnfield/flutter_path_drawing/27ce967273429ce505962d4cf68c17a5b16b5aef/golden/31.png -------------------------------------------------------------------------------- /golden/32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnfield/flutter_path_drawing/27ce967273429ce505962d4cf68c17a5b16b5aef/golden/32.png -------------------------------------------------------------------------------- /golden/33.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnfield/flutter_path_drawing/27ce967273429ce505962d4cf68c17a5b16b5aef/golden/33.png -------------------------------------------------------------------------------- /golden/34.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnfield/flutter_path_drawing/27ce967273429ce505962d4cf68c17a5b16b5aef/golden/34.png -------------------------------------------------------------------------------- /golden/35.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnfield/flutter_path_drawing/27ce967273429ce505962d4cf68c17a5b16b5aef/golden/35.png -------------------------------------------------------------------------------- /golden/36.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnfield/flutter_path_drawing/27ce967273429ce505962d4cf68c17a5b16b5aef/golden/36.png -------------------------------------------------------------------------------- /golden/37.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnfield/flutter_path_drawing/27ce967273429ce505962d4cf68c17a5b16b5aef/golden/37.png -------------------------------------------------------------------------------- /golden/38.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnfield/flutter_path_drawing/27ce967273429ce505962d4cf68c17a5b16b5aef/golden/38.png -------------------------------------------------------------------------------- /golden/39.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnfield/flutter_path_drawing/27ce967273429ce505962d4cf68c17a5b16b5aef/golden/39.png -------------------------------------------------------------------------------- /golden/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnfield/flutter_path_drawing/27ce967273429ce505962d4cf68c17a5b16b5aef/golden/4.png -------------------------------------------------------------------------------- /golden/40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnfield/flutter_path_drawing/27ce967273429ce505962d4cf68c17a5b16b5aef/golden/40.png -------------------------------------------------------------------------------- /golden/41.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnfield/flutter_path_drawing/27ce967273429ce505962d4cf68c17a5b16b5aef/golden/41.png -------------------------------------------------------------------------------- /golden/42.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnfield/flutter_path_drawing/27ce967273429ce505962d4cf68c17a5b16b5aef/golden/42.png -------------------------------------------------------------------------------- /golden/43.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnfield/flutter_path_drawing/27ce967273429ce505962d4cf68c17a5b16b5aef/golden/43.png -------------------------------------------------------------------------------- /golden/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnfield/flutter_path_drawing/27ce967273429ce505962d4cf68c17a5b16b5aef/golden/5.png -------------------------------------------------------------------------------- /golden/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnfield/flutter_path_drawing/27ce967273429ce505962d4cf68c17a5b16b5aef/golden/6.png -------------------------------------------------------------------------------- /golden/7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnfield/flutter_path_drawing/27ce967273429ce505962d4cf68c17a5b16b5aef/golden/7.png -------------------------------------------------------------------------------- /golden/8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnfield/flutter_path_drawing/27ce967273429ce505962d4cf68c17a5b16b5aef/golden/8.png -------------------------------------------------------------------------------- /golden/9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnfield/flutter_path_drawing/27ce967273429ce505962d4cf68c17a5b16b5aef/golden/9.png -------------------------------------------------------------------------------- /lib/path_drawing.dart: -------------------------------------------------------------------------------- 1 | export 'package:path_drawing/src/dash_path.dart'; 2 | export 'package:path_drawing/src/parse_path.dart'; 3 | export 'package:path_drawing/src/trim_path.dart'; 4 | -------------------------------------------------------------------------------- /lib/src/dash_path.dart: -------------------------------------------------------------------------------- 1 | import 'dart:ui'; 2 | 3 | /// Creates a new path that is drawn from the segments of `source`. 4 | /// 5 | /// Dash intervals are controled by the `dashArray` - see [CircularIntervalList] 6 | /// for examples. 7 | /// 8 | /// `dashOffset` specifies an initial starting point for the dashing. 9 | /// 10 | /// Passing a `source` that is an empty path will return an empty path. 11 | Path dashPath( 12 | Path source, { 13 | required CircularIntervalList dashArray, 14 | DashOffset? dashOffset, 15 | }) { 16 | assert(dashArray != null); // ignore: unnecessary_null_comparison 17 | 18 | dashOffset = dashOffset ?? const DashOffset.absolute(0.0); 19 | // TODO: Is there some way to determine how much of a path would be visible today? 20 | 21 | final Path dest = Path(); 22 | for (final PathMetric metric in source.computeMetrics()) { 23 | double distance = dashOffset._calculate(metric.length); 24 | bool draw = true; 25 | while (distance < metric.length) { 26 | final double len = dashArray.next; 27 | if (draw) { 28 | dest.addPath(metric.extractPath(distance, distance + len), Offset.zero); 29 | } 30 | distance += len; 31 | draw = !draw; 32 | } 33 | } 34 | 35 | return dest; 36 | } 37 | 38 | enum _DashOffsetType { Absolute, Percentage } 39 | 40 | /// Specifies the starting position of a dash array on a path, either as a 41 | /// percentage or absolute value. 42 | /// 43 | /// The internal value will be guaranteed to not be null. 44 | class DashOffset { 45 | /// Create a DashOffset that will be measured as a percentage of the length 46 | /// of the segment being dashed. 47 | /// 48 | /// `percentage` will be clamped between 0.0 and 1.0. 49 | DashOffset.percentage(double percentage) 50 | : _rawVal = percentage.clamp(0.0, 1.0), 51 | _dashOffsetType = _DashOffsetType.Percentage; 52 | 53 | /// Create a DashOffset that will be measured in terms of absolute pixels 54 | /// along the length of a [Path] segment. 55 | const DashOffset.absolute(double start) 56 | : _rawVal = start, 57 | _dashOffsetType = _DashOffsetType.Absolute; 58 | 59 | final double _rawVal; 60 | final _DashOffsetType _dashOffsetType; 61 | 62 | double _calculate(double length) { 63 | return _dashOffsetType == _DashOffsetType.Absolute 64 | ? _rawVal 65 | : length * _rawVal; 66 | } 67 | 68 | @override 69 | bool operator ==(Object other) { 70 | if (identical(this, other)) { 71 | return true; 72 | } 73 | 74 | return other is DashOffset && 75 | other._rawVal == _rawVal && 76 | other._dashOffsetType == _dashOffsetType; 77 | } 78 | 79 | @override 80 | int get hashCode => Object.hash(_rawVal, _dashOffsetType); 81 | } 82 | 83 | /// A circular array of dash offsets and lengths. 84 | /// 85 | /// For example, the array `[5, 10]` would result in dashes 5 pixels long 86 | /// followed by blank spaces 10 pixels long. The array `[5, 10, 5]` would 87 | /// result in a 5 pixel dash, a 10 pixel gap, a 5 pixel dash, a 5 pixel gap, 88 | /// a 10 pixel dash, etc. 89 | /// 90 | /// Note that this does not quite conform to an [Iterable], because it does 91 | /// not have a moveNext. 92 | class CircularIntervalList { 93 | CircularIntervalList(this._vals); 94 | 95 | final List _vals; 96 | int _idx = 0; 97 | 98 | T get next { 99 | if (_idx >= _vals.length) { 100 | _idx = 0; 101 | } 102 | return _vals[_idx++]; 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /lib/src/parse_path.dart: -------------------------------------------------------------------------------- 1 | import 'dart:ui' show Path; 2 | 3 | import 'package:path_parsing/path_parsing.dart'; 4 | 5 | /// Creates a [Path] object from an SVG data string. 6 | /// 7 | /// Passing an empty string will result in an empty path. 8 | Path parseSvgPathData(String svg) { 9 | if (svg == '') { 10 | return Path(); 11 | } 12 | 13 | final SvgPathStringSource parser = SvgPathStringSource(svg); 14 | final FlutterPathProxy path = FlutterPathProxy(); 15 | final SvgPathNormalizer normalizer = SvgPathNormalizer(); 16 | for (PathSegmentData seg in parser.parseSegments()) { 17 | normalizer.emitSegment(seg, path); 18 | } 19 | return path.path; 20 | } 21 | 22 | /// A [PathProxy] that takes the output of the path parsing library 23 | /// and maps it to a dart:ui [Path]. 24 | class FlutterPathProxy extends PathProxy { 25 | FlutterPathProxy({Path? p}) : path = p ?? Path(); 26 | 27 | final Path path; 28 | 29 | @override 30 | void close() { 31 | path.close(); 32 | } 33 | 34 | @override 35 | void cubicTo( 36 | double x1, double y1, double x2, double y2, double x3, double y3) { 37 | path.cubicTo(x1, y1, x2, y2, x3, y3); 38 | } 39 | 40 | @override 41 | void lineTo(double x, double y) { 42 | path.lineTo(x, y); 43 | } 44 | 45 | @override 46 | void moveTo(double x, double y) { 47 | path.moveTo(x, y); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /lib/src/trim_path.dart: -------------------------------------------------------------------------------- 1 | import 'dart:ui'; 2 | 3 | /// The point on the path to trim from. 4 | enum PathTrimOrigin { 5 | /// Specifies that trimming should start from the first point in a segment. 6 | begin, 7 | 8 | /// Specifies that trimming should start from the last point in a segment. 9 | end 10 | } 11 | 12 | /// Trims `percentage` of the `source` [Path] away and returns a new path. 13 | /// 14 | /// The `percentage` parameter will be clamped between 0..1 and must not be null. 15 | /// 16 | /// Use the `firstOnly` parameter to specify whether this should apply only 17 | /// to the first segment of the path (and thus return only the first trimmed 18 | /// segment) or all segments of the path. Multi-segment paths (i.e. paths with a 19 | /// move verb) will all be trimmed if this is false; otherwise, a trimmed version 20 | /// of only the first path segment will be returned. It must not be null. 21 | /// 22 | /// The `origin` parameter allows the user to control which end of the path will be 23 | /// trimmed. It must not be null. 24 | /// 25 | /// If `source` is empty, an empty path will be returned. 26 | Path trimPath( 27 | Path source, 28 | double percentage, { 29 | bool firstOnly = true, 30 | PathTrimOrigin origin = PathTrimOrigin.begin, 31 | }) { 32 | assert(percentage != null); // ignore: unnecessary_null_comparison 33 | assert(firstOnly != null); // ignore: unnecessary_null_comparison 34 | assert(origin != null); // ignore: unnecessary_null_comparison 35 | 36 | percentage = percentage.clamp(0.0, 1.0); 37 | if (percentage == 1.0) { 38 | return Path(); 39 | } 40 | if (percentage == 0.0) { 41 | return Path.from(source); 42 | } 43 | if (origin == PathTrimOrigin.end) { 44 | percentage = 1.0 - percentage; 45 | } 46 | 47 | final Path dest = Path(); 48 | for (final PathMetric metric in source.computeMetrics()) { 49 | switch (origin) { 50 | case PathTrimOrigin.end: 51 | dest.addPath( 52 | metric.extractPath(0.0, metric.length * percentage), 53 | Offset.zero, 54 | ); 55 | break; 56 | case PathTrimOrigin.begin: 57 | dest.addPath( 58 | metric.extractPath(metric.length * percentage, metric.length), 59 | Offset.zero, 60 | ); 61 | break; 62 | } 63 | if (firstOnly) { 64 | break; 65 | } 66 | } 67 | 68 | return dest; 69 | } 70 | -------------------------------------------------------------------------------- /path_drawing.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /path_drawing_android.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: path_drawing 2 | version: 1.0.1 3 | description: > 4 | A flutter library to help with (Canvas) Path creation and manipulation 5 | homepage: https://github.com/dnfield/flutter_path_drawing 6 | 7 | dependencies: 8 | vector_math: ^2.1.0 9 | meta: ^1.3.0 10 | path_parsing: ^1.0.1 11 | flutter: 12 | sdk: flutter 13 | 14 | dev_dependencies: 15 | path: ^1.8.0 16 | test: ^1.16.0 17 | flutter_test: 18 | sdk: flutter 19 | 20 | 21 | # For information on the generic Dart part of this file, see the 22 | # following page: https://www.dartlang.org/tools/pub/pubspec 23 | 24 | # The following section is specific to Flutter. 25 | flutter: 26 | # There's no need to include these as we don't use them. 27 | # This fixes an issue when our plugin is imported into a project that also 28 | # uses no material icons. 29 | uses-material-design: false 30 | 31 | environment: 32 | sdk: '>=2.12.0 <3.0.0' 33 | flutter: '>=1.24.0-7.0' 34 | -------------------------------------------------------------------------------- /test/dash_path_test.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: prefer_const_constructors 2 | 3 | import 'dart:ui' show Path; 4 | 5 | import 'package:path_drawing/path_drawing.dart'; 6 | 7 | import 'package:test/test.dart'; 8 | 9 | void main() { 10 | test('CircularList tests', () { 11 | final List ints = [1, 2, 3]; 12 | 13 | final CircularIntervalList list = CircularIntervalList(ints); 14 | 15 | expect(list.next, 1); 16 | expect(list.next, 2); 17 | expect(list.next, 3); 18 | expect(list.next, 1); 19 | expect(list.next, 2); 20 | expect(list.next, 3); 21 | }); 22 | 23 | test('DashPath tests', () { 24 | final Path singleSegmentLine = Path()..lineTo(10.0, 10.0); 25 | final CircularIntervalList dashArray = 26 | CircularIntervalList([1.0, 5.0]); 27 | 28 | expect(dashPath(singleSegmentLine, dashArray: dashArray), isNotNull); 29 | expect( 30 | dashPath( 31 | singleSegmentLine, 32 | dashArray: dashArray, 33 | dashOffset: DashOffset.percentage(5.0), 34 | ), 35 | isNotNull, 36 | ); 37 | }); 38 | 39 | group('DashOffset supports value equality', () { 40 | test('absolute', () { 41 | expect( 42 | DashOffset.absolute(20), 43 | equals(DashOffset.absolute(20)), 44 | ); 45 | }); 46 | 47 | test('percentage', () { 48 | expect( 49 | DashOffset.percentage(0.2), 50 | equals(DashOffset.percentage(0.2)), 51 | ); 52 | }); 53 | }); 54 | } 55 | -------------------------------------------------------------------------------- /test/render_path_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | import 'dart:typed_data'; 3 | 4 | import 'package:path/path.dart'; 5 | 6 | import 'package:test/test.dart'; 7 | 8 | import '../tool/path_to_image.dart'; 9 | 10 | void main() { 11 | test('Path rendering matches golden files', () async { 12 | for (int i = 0; i < paths.length; i++) { 13 | final Uint8List bytes = await getPathPngBytes(paths[i]); 14 | final File golden = File(join( 15 | dirname(Platform.script.path), 16 | dirname(Platform.script.path).endsWith('test') ? '..' : '', 17 | 'golden', 18 | '$i.png', 19 | )); 20 | golden.writeAsBytesSync(bytes); 21 | final Uint8List goldenBytes = await golden.readAsBytes(); 22 | 23 | expect(bytes, orderedEquals(goldenBytes)); 24 | } 25 | }); 26 | } 27 | -------------------------------------------------------------------------------- /test/trim_path_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:ui' show Path, Rect; 2 | 3 | import 'package:flutter_test/flutter_test.dart'; 4 | import 'package:path_drawing/path_drawing.dart'; 5 | 6 | void main() { 7 | test('TrimPath tests', () { 8 | final Path singleSegmentLine = Path()..lineTo(10.0, 10.0); 9 | final Path multiSegmentLine = Path() 10 | ..lineTo(10.0, 10.0) 11 | ..moveTo(50.0, 10.0) 12 | ..lineTo(20.0, 20.0); 13 | 14 | expect(trimPath(singleSegmentLine, 0.5).getBounds(), 15 | const Rect.fromLTRB(5, 5, 10, 10)); 16 | expect(trimPath(singleSegmentLine, 1.0).getBounds(), Rect.zero); 17 | expect(trimPath(singleSegmentLine, 0.0).getBounds(), 18 | singleSegmentLine.getBounds()); 19 | expect(trimPath(singleSegmentLine, 0.5, origin: PathTrimOrigin.end), 20 | isNotNull); 21 | expect(trimPath(singleSegmentLine, 0.5, firstOnly: false), isNotNull); 22 | 23 | expect(trimPath(multiSegmentLine, 0.5, firstOnly: false).getBounds(), 24 | const Rect.fromLTRB(5, 5, 35, 20)); 25 | expect(trimPath(multiSegmentLine, 0.5).getBounds(), 26 | const Rect.fromLTRB(5, 5, 10, 10)); 27 | }); 28 | } 29 | -------------------------------------------------------------------------------- /tool/path_to_image.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:io'; 3 | import 'dart:math' show max; 4 | import 'dart:typed_data'; 5 | import 'dart:ui'; 6 | 7 | import 'package:path/path.dart'; 8 | 9 | import 'package:path_drawing/path_drawing.dart'; 10 | 11 | const List paths = [ 12 | 'M100,200 L3,4', 13 | 'M100,200 l3,4', 14 | 'M100,200 H3', 15 | 'M100,200 h3', 16 | 'M100,200 V3', 17 | 'M100,200 v3', 18 | 'M100,200 C3,4,5,6,7,8', 19 | 'M100,200 c3,4,5,6,7,8', 20 | 'M100,200 S3,4,5,6', 21 | 'M100,200 s3,4,5,6', 22 | 'M100,200 Q3,4,5,6', 23 | 'M100,200 q3,4,5,6', 24 | 'M100,200 T3,4', 25 | 'M100,200 t3,4', 26 | 'M100,200 A3,4,5,0,0,6,7', 27 | 'M100,200 A3,4,5,1,0,6,7', 28 | 'M100,200 A3,4,5,0,1,6,7', 29 | 'M100,200 A3,4,5,1,1,6,7', 30 | 'M100,200 a3,4,5,0,0,6,7', 31 | 'M100,200 a3,4,5,0,1,6,7', 32 | 'M100,200 a3,4,5,1,0,6,7', 33 | 'M100,200 a3,4,5,1,1,6,7', 34 | 'M100,200 a3,4,5,006,7', 35 | 'M100,200 a3,4,5,016,7', 36 | 'M100,200 a3,4,5,106,7', 37 | 'M100,200 a3,4,5,116,7', 38 | '''M19.0281,19.40466 20.7195,19.40466 20.7195,15.71439 24.11486,15.71439 24.11486,14.36762 20.7195,14.36762 39 | 20.7195,11.68641 24.74134,11.68641 24.74134,10.34618 19.0281,10.34618 z''', 40 | 'M100,200 a0,4,5,0,0,10,0 a4,0,5,0,0,0,10 a0,0,5,0,0,-10,0 z', 41 | 'M.1 .2 L.3 .4 .5 .6', 42 | 'M1,1h2,3', 43 | 'M1,1H2,3', 44 | 'M1,1v2,3', 45 | 'M1,1V2,3', 46 | 'M1,1c2,3 4,5 6,7 8,9 10,11 12,13', 47 | 'M1,1C2,3 4,5 6,7 8,9 10,11 12,13', 48 | 'M1,1s2,3 4,5 6,7 8,9', 49 | 'M1,1S2,3 4,5 6,7 8,9', 50 | 'M1,1q2,3 4,5 6,7 8,9', 51 | 'M1,1Q2,3 4,5 6,7 8,9', 52 | 'M1,1t2,3 4,5', 53 | 'M1,1T2,3 4,5', 54 | 'M1,1a2,3,4,0,0,5,6 7,8,9,0,0,10,11', 55 | 'M1,1A2,3,4,0,0,5,6 7,8,9,0,0,10,11', 56 | 'm18 11.8a.41.41 0 0 1 .24.08l.59.43h.05.72a.4.4 0 0 1 .39.28l.22.69a.08.08 0 0 0 0 0l.58.43a.41.41 0 0 1 .15.45l-.22.68a.09.09 0 0 0 0 .07l.22.68a.4.4 0 0 1 -.15.46l-.58.42a.1.1 0 0 0 0 0l-.22.68a.41.41 0 0 1 -.38.29h-.79l-.58.43a.41.41 0 0 1 -.24.08.46.46 0 0 1 -.24-.08l-.58-.43h-.06-.72a.41.41 0 0 1 -.39-.28l-.22-.68a.1.1 0 0 0 0 0l-.58-.43a.42.42 0 0 1 -.15-.46l.23-.67v-.02l-.29-.68a.43.43 0 0 1 .15-.46l.58-.42a.1.1 0 0 0 0-.05l.27-.69a.42.42 0 0 1 .39-.28h.78l.58-.43a.43.43 0 0 1 .25-.09m0-1a1.37 1.37 0 0 0 -.83.27l-.34.25h-.43a1.42 1.42 0 0 0 -1.34 1l-.13.4-.35.25a1.42 1.42 0 0 0 -.51 1.58l.13.4-.13.4a1.39 1.39 0 0 0 .52 1.59l.34.25.13.4a1.41 1.41 0 0 0 1.34 1h.43l.34.26a1.44 1.44 0 0 0 .83.27 1.38 1.38 0 0 0 .83-.28l.35-.24h.43a1.4 1.4 0 0 0 1.33-1l.13-.4.35-.26a1.39 1.39 0 0 0 .51-1.57l-.13-.4.13-.41a1.4 1.4 0 0 0 -.51-1.56l-.35-.25-.13-.41a1.4 1.4 0 0 0 -1.34-1h-.42l-.34-.26a1.43 1.43 0 0 0 -.84-.28z', 57 | ]; 58 | final Paint blackStrokePaint = Paint() 59 | ..color = const Color.fromARGB(255, 0, 0, 0) 60 | ..strokeWidth = 1.0 61 | ..style = PaintingStyle.stroke; 62 | final Paint whiteFillPaint = Paint() 63 | ..color = const Color.fromARGB(255, 255, 255, 255) 64 | ..style = PaintingStyle.fill; 65 | 66 | Future getPathPngBytes(String pathString) async { 67 | final PictureRecorder rec = PictureRecorder(); 68 | final Canvas canvas = Canvas(rec); 69 | 70 | final Path p = parseSvgPathData(pathString); 71 | 72 | final Rect bounds = p.getBounds(); 73 | const double scaleFactor = 5.0; 74 | canvas.scale(scaleFactor); 75 | canvas.drawPaint(whiteFillPaint); 76 | 77 | canvas.drawPath(p, blackStrokePaint); 78 | 79 | final Picture pict = rec.endRecording(); 80 | 81 | final int imgWidth = 82 | (max(bounds.width, bounds.right) * 2 * scaleFactor).ceil(); 83 | final int imgHeight = 84 | (max(bounds.height, bounds.bottom) * 2 * scaleFactor).ceil(); 85 | 86 | final Image image = await pict.toImage(imgWidth, imgHeight); 87 | final ByteData bytes = (await image.toByteData(format: ImageByteFormat.png))!; 88 | 89 | return bytes.buffer.asUint8List(); 90 | } 91 | 92 | Future main() async { 93 | for (int i = 0; i < paths.length; i++) { 94 | final String pathName = 95 | join(dirname(Platform.script.path), 'golden', '$i.png'); 96 | final File output = File(pathName); 97 | await output.writeAsBytes(await getPathPngBytes(paths[i])); 98 | } 99 | } 100 | --------------------------------------------------------------------------------