├── .github └── workflows │ └── deploy_workflow.yml ├── .gitignore ├── .metadata ├── README.md ├── analysis_options.yaml ├── assets └── svg │ └── github.svg ├── fonts ├── Outfit-Bold.ttf ├── Outfit-ExtraBold.ttf ├── Outfit-ExtraLight.ttf ├── Outfit-Light.ttf ├── Outfit-Medium.ttf ├── Outfit-Regular.ttf ├── Outfit-SemiBold.ttf └── Outfit-Thin.ttf ├── lib ├── main.dart ├── model │ └── curve_model.dart ├── utils │ ├── extension │ │ └── string.dart │ └── theme │ │ ├── colors.dart │ │ ├── theme.dart │ │ └── theme_provider.dart └── views │ ├── home_page.dart │ └── widgets │ ├── animated_box │ ├── animated_box_widget.dart │ ├── animated_boxes.dart │ └── provider.dart │ ├── appbar.dart │ ├── code_block.dart │ ├── cubic_curve_input_widget.dart │ ├── cubic_graph.dart │ ├── dropdown_menu.dart │ ├── graph │ ├── graph_config.dart │ ├── graph_painter.dart │ └── graph_widget.dart │ ├── screen_mode.dart │ └── time_slider.dart ├── makefile ├── pubspec.lock ├── pubspec.yaml ├── test └── widget_test.dart └── web ├── favicon.png ├── icons ├── Icon-192.png ├── Icon-512.png ├── Icon-maskable-192.png └── Icon-maskable-512.png ├── index.html └── manifest.json /.github/workflows/deploy_workflow.yml: -------------------------------------------------------------------------------- 1 | name: Deploy to GitHub 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - master 8 | 9 | jobs: 10 | deploy-web: 11 | name: Deploy to Github 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Clone repository 15 | uses: actions/checkout@v4 16 | 17 | - name: Setting up Flutter Environment 18 | uses: subosito/flutter-action@v2 19 | with: 20 | channel: "stable" 21 | 22 | - name: Flutter Version 23 | run: flutter --version 24 | 25 | - name: Cleaning Project 26 | run: flutter clean 27 | 28 | - name: Installing Dependencies 29 | run: flutter pub get 30 | 31 | - name: Creating a build 32 | run: flutter build web --wasm --release --base-href "/flutter-curve-visualizer/" 33 | 34 | - name: Deploying to GitHub Pages 35 | uses: peaceiris/actions-gh-pages@v4 36 | with: 37 | github_token: ${{ secrets.GITHUB_TOKEN }} 38 | publish_dir: ./build/web 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .build/ 9 | .buildlog/ 10 | .history 11 | .svn/ 12 | .swiftpm/ 13 | migrate_working_dir/ 14 | 15 | # IntelliJ related 16 | *.iml 17 | *.ipr 18 | *.iws 19 | .idea/ 20 | 21 | # The .vscode folder contains launch configuration and tasks you configure in 22 | # VS Code which you may wish to be included in version control, so this line 23 | # is commented out by default. 24 | #.vscode/ 25 | 26 | # Flutter/Dart/Pub related 27 | **/doc/api/ 28 | **/ios/Flutter/.last_build_id 29 | .dart_tool/ 30 | .flutter-plugins 31 | .flutter-plugins-dependencies 32 | .pub-cache/ 33 | .pub/ 34 | /build/ 35 | 36 | # Symbolication related 37 | app.*.symbols 38 | 39 | # Obfuscation related 40 | app.*.map.json 41 | 42 | # Android Studio will place build artifacts here 43 | /android/app/debug 44 | /android/app/profile 45 | /android/app/release 46 | -------------------------------------------------------------------------------- /.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: "8495dee1fd4aacbe9de707e7581203232f591b2f" 8 | channel: "stable" 9 | 10 | project_type: app 11 | 12 | # Tracks metadata for the flutter migrate command 13 | migration: 14 | platforms: 15 | - platform: root 16 | create_revision: 8495dee1fd4aacbe9de707e7581203232f591b2f 17 | base_revision: 8495dee1fd4aacbe9de707e7581203232f591b2f 18 | - platform: web 19 | create_revision: 8495dee1fd4aacbe9de707e7581203232f591b2f 20 | base_revision: 8495dee1fd4aacbe9de707e7581203232f591b2f 21 | 22 | # User provided section 23 | 24 | # List of Local paths (relative to this file) that should be 25 | # ignored by the migrate tool. 26 | # 27 | # Files that are not part of the templates will be ignored by default. 28 | unmanaged_files: 29 | - 'lib/main.dart' 30 | - 'ios/Runner.xcodeproj/project.pbxproj' 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Flutter Curve Visualizer 2 | 3 | A web app built with Flutter to visualize animation curves interactively. This tool helps developers 4 | understand and experiment with different Flutter animation curves in a graphical interface. 5 | 6 | ## Features 7 | 8 | - Graphical visualization of animation curves on a graph. 9 | - Interactive controls to select curve category, curve type, and animation duration. 10 | - Play/Pause animation functionality using a Floating Action Button (FAB). 11 | - Preview boxes demonstrating Translate X & Y, Scale, Rotate, Flip and Opacity effects. 12 | - Responsive design for seamless use across devices (desktop, tablet, and mobile). 13 | 14 | This app is ideal for exploring and learning how different animation curves work in Flutter 15 | applications. 16 | 17 | ## Roadmap 18 | 19 | - Add more controls. 20 | 21 | - Add Interactive Graph to visualize custom curve. 22 | 23 | - Add Switch to toggle theme mode in header. 24 | 25 | ## Live-Demo 26 | 27 | https://vchib1.github.io/flutter-curve-visualizer/ 28 | 29 | ## Authors 30 | 31 | - [@vchib1](https://www.github.com/vchib1) 32 | 33 | -------------------------------------------------------------------------------- /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 https://dart.dev/lints. 17 | # 18 | # Instead of disabling a lint rule for the entire project in the 19 | # section below, it can also be suppressed for a single line of code 20 | # or a specific dart file by using the `// ignore: name_of_lint` and 21 | # `// ignore_for_file: name_of_lint` syntax on the line or in the file 22 | # producing the lint. 23 | rules: 24 | # avoid_print: false # Uncomment to disable the `avoid_print` rule 25 | # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule 26 | 27 | # Additional information about this file can be found at 28 | # https://dart.dev/guides/language/analysis-options 29 | -------------------------------------------------------------------------------- /assets/svg/github.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | -------------------------------------------------------------------------------- /fonts/Outfit-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vchib1/flutter-curve-visualizer/fe547667e37b6e0c8283ece35958de785dd26455/fonts/Outfit-Bold.ttf -------------------------------------------------------------------------------- /fonts/Outfit-ExtraBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vchib1/flutter-curve-visualizer/fe547667e37b6e0c8283ece35958de785dd26455/fonts/Outfit-ExtraBold.ttf -------------------------------------------------------------------------------- /fonts/Outfit-ExtraLight.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vchib1/flutter-curve-visualizer/fe547667e37b6e0c8283ece35958de785dd26455/fonts/Outfit-ExtraLight.ttf -------------------------------------------------------------------------------- /fonts/Outfit-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vchib1/flutter-curve-visualizer/fe547667e37b6e0c8283ece35958de785dd26455/fonts/Outfit-Light.ttf -------------------------------------------------------------------------------- /fonts/Outfit-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vchib1/flutter-curve-visualizer/fe547667e37b6e0c8283ece35958de785dd26455/fonts/Outfit-Medium.ttf -------------------------------------------------------------------------------- /fonts/Outfit-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vchib1/flutter-curve-visualizer/fe547667e37b6e0c8283ece35958de785dd26455/fonts/Outfit-Regular.ttf -------------------------------------------------------------------------------- /fonts/Outfit-SemiBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vchib1/flutter-curve-visualizer/fe547667e37b6e0c8283ece35958de785dd26455/fonts/Outfit-SemiBold.ttf -------------------------------------------------------------------------------- /fonts/Outfit-Thin.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vchib1/flutter-curve-visualizer/fe547667e37b6e0c8283ece35958de785dd26455/fonts/Outfit-Thin.ttf -------------------------------------------------------------------------------- /lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_animate/flutter_animate.dart'; 3 | import 'package:flutter_curve_visualizer/views/widgets/screen_mode.dart'; 4 | import 'package:flutter_curve_visualizer/utils/theme/theme.dart'; 5 | import 'package:provider/provider.dart'; 6 | import 'package:shared_preferences/shared_preferences.dart'; 7 | import 'utils/theme/theme_provider.dart'; 8 | import 'views/home_page.dart'; 9 | import 'views/widgets/animated_box/provider.dart'; 10 | 11 | Future main() async { 12 | WidgetsFlutterBinding.ensureInitialized(); 13 | 14 | final pref = await SharedPreferences.getInstance(); 15 | 16 | runApp( 17 | MultiProvider( 18 | providers: [ 19 | ChangeNotifierProvider(create: (context) => ThemeProvider(pref: pref)), 20 | ChangeNotifierProvider( 21 | create: (context) => AnimatedBoxesProvider(pref: pref)), 22 | ], 23 | child: const MyApp(), 24 | ), 25 | ); 26 | } 27 | 28 | class MyApp extends StatelessWidget { 29 | const MyApp({super.key}); 30 | 31 | ScreenMode getLayoutType(double width) { 32 | if (width < 480) { 33 | return ScreenMode.mobile; 34 | } else if (width < 850) { 35 | return ScreenMode.tablet; 36 | } else { 37 | return ScreenMode.web; 38 | } 39 | } 40 | 41 | @override 42 | Widget build(BuildContext context) { 43 | final theme = MaterialTheme(Theme.of(context).textTheme); 44 | 45 | return Consumer( 46 | builder: (context, themeProvider, child) { 47 | return MaterialApp( 48 | title: 'Flutter Curve Visualizer', 49 | debugShowCheckedModeBanner: false, 50 | themeMode: themeProvider.getThemeMode(), 51 | theme: theme.lightMediumContrast(), 52 | darkTheme: theme.dark(), 53 | home: LayoutBuilder( 54 | builder: (context, constraints) { 55 | return ScreenModeWidget( 56 | mode: getLayoutType(constraints.maxWidth), 57 | child: const HomePage(), 58 | ); 59 | }, 60 | ), 61 | ).animate().fadeIn(); 62 | }, 63 | ); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /lib/model/curve_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | class CurveModel extends Equatable { 5 | final Curve curve; 6 | final String code; 7 | final String name; 8 | final bool isCustom; 9 | 10 | const CurveModel({ 11 | required this.curve, 12 | required this.code, 13 | required this.name, 14 | this.isCustom = false, 15 | }); 16 | 17 | @override 18 | List get props => [isCustom, name]; 19 | 20 | static Map> get list => { 21 | 'ease': easeCurves, 22 | 'bounces': bounceCurves, 23 | 'elastics': elasticCurves, 24 | 'others': otherCurves, 25 | }; 26 | 27 | CurveModel copyWith({ 28 | Curve? curve, 29 | String? code, 30 | String? name, 31 | bool? isCustom, 32 | }) { 33 | return CurveModel( 34 | curve: curve ?? this.curve, 35 | code: code ?? this.code, 36 | name: name ?? this.name, 37 | isCustom: isCustom ?? this.isCustom, 38 | ); 39 | } 40 | } 41 | 42 | final easeCurves = [ 43 | CurveModel(curve: Curves.ease, code: "Curves.ease", name: "ease"), 44 | CurveModel(curve: Curves.easeIn, code: "Curves.easeIn", name: "easeIn"), 45 | CurveModel( 46 | curve: Curves.easeInToLinear, 47 | code: "Curves.easeInToLinear", 48 | name: "easeInToLinear"), 49 | CurveModel( 50 | curve: Curves.easeInSine, code: "Curves.easeInSine", name: "easeInSine"), 51 | CurveModel( 52 | curve: Curves.easeInQuad, code: "Curves.easeInQuad", name: "easeInQuad"), 53 | CurveModel( 54 | curve: Curves.easeInCubic, 55 | code: "Curves.easeInCubic", 56 | name: "easeInCubic"), 57 | CurveModel( 58 | curve: Curves.easeInQuart, 59 | code: "Curves.easeInQuart", 60 | name: "easeInQuart"), 61 | CurveModel( 62 | curve: Curves.easeInQuint, 63 | code: "Curves.easeInQuint", 64 | name: "easeInQuint"), 65 | CurveModel( 66 | curve: Curves.easeInExpo, code: "Curves.easeInExpo", name: "easeInExpo"), 67 | CurveModel( 68 | curve: Curves.easeInCirc, code: "Curves.easeInCirc", name: "easeInCirc"), 69 | CurveModel( 70 | curve: Curves.easeInBack, code: "Curves.easeInBack", name: "easeInBack"), 71 | CurveModel(curve: Curves.easeOut, code: "Curves.easeOut", name: "easeOut"), 72 | CurveModel( 73 | curve: Curves.linearToEaseOut, 74 | code: "Curves.linearToEaseOut", 75 | name: "linearToEaseOut"), 76 | CurveModel( 77 | curve: Curves.easeOutSine, 78 | code: "Curves.easeOutSine", 79 | name: "easeOutSine"), 80 | CurveModel( 81 | curve: Curves.easeOutQuad, 82 | code: "Curves.easeOutQuad", 83 | name: "easeOutQuad"), 84 | CurveModel( 85 | curve: Curves.easeOutCubic, 86 | code: "Curves.easeOutCubic", 87 | name: "easeOutCubic"), 88 | CurveModel( 89 | curve: Curves.easeOutQuart, 90 | code: "Curves.easeOutQuart", 91 | name: "easeOutQuart"), 92 | CurveModel( 93 | curve: Curves.easeOutQuint, 94 | code: "Curves.easeOutQuint", 95 | name: "easeOutQuint"), 96 | CurveModel( 97 | curve: Curves.easeOutExpo, 98 | code: "Curves.easeOutExpo", 99 | name: "easeOutExpo"), 100 | CurveModel( 101 | curve: Curves.easeOutCirc, 102 | code: "Curves.easeOutCirc", 103 | name: "easeOutCirc"), 104 | CurveModel( 105 | curve: Curves.easeOutBack, 106 | code: "Curves.easeOutBack", 107 | name: "easeOutBack"), 108 | CurveModel( 109 | curve: Curves.easeInOut, code: "Curves.easeInOut", name: "easeInOut"), 110 | CurveModel( 111 | curve: Curves.easeInOutSine, 112 | code: "Curves.easeInOutSine", 113 | name: "easeInOutSine"), 114 | CurveModel( 115 | curve: Curves.easeInOutQuad, 116 | code: "Curves.easeInOutQuad", 117 | name: "easeInOutQuad"), 118 | CurveModel( 119 | curve: Curves.easeInOutCubic, 120 | code: "Curves.easeInOutCubic", 121 | name: "easeInOutCubic"), 122 | CurveModel( 123 | curve: Curves.easeInOutCubicEmphasized, 124 | code: "Curves.easeInOutCubicEmphasized", 125 | name: "easeInOutCubicEmphasized"), 126 | CurveModel( 127 | curve: Curves.easeInOutQuart, 128 | code: "Curves.easeInOutQuart", 129 | name: "easeInOutQuart"), 130 | CurveModel( 131 | curve: Curves.easeInOutQuint, 132 | code: "Curves.easeInOutQuint", 133 | name: "easeInOutQuint"), 134 | CurveModel( 135 | curve: Curves.easeInOutExpo, 136 | code: "Curves.easeInOutExpo", 137 | name: "easeInOutExpo"), 138 | CurveModel( 139 | curve: Curves.easeInOutCirc, 140 | code: "Curves.easeInOutCirc", 141 | name: "easeInOutCirc"), 142 | CurveModel( 143 | curve: Curves.easeInOutBack, 144 | code: "Curves.easeInOutBack", 145 | name: "easeInOutBack"), 146 | ]; 147 | 148 | final bounceCurves = [ 149 | CurveModel( 150 | curve: Curves.bounceIn, 151 | code: "Curves.bounceIn", 152 | name: "bounceIn", 153 | ), 154 | CurveModel( 155 | curve: Curves.bounceOut, 156 | code: "Curves.bounceOut", 157 | name: "bounceOut", 158 | ), 159 | CurveModel( 160 | curve: Curves.bounceInOut, 161 | code: "Curves.bounceInOut", 162 | name: "bounceInOut", 163 | ), 164 | ]; 165 | 166 | final elasticCurves = [ 167 | CurveModel( 168 | curve: Curves.elasticIn, 169 | code: "Curves.elasticIn", 170 | name: "elasticIn", 171 | ), 172 | CurveModel( 173 | curve: Curves.elasticOut, 174 | code: "Curves.elasticOut", 175 | name: "elasticOut", 176 | ), 177 | CurveModel( 178 | curve: Curves.elasticInOut, 179 | code: "Curves.elasticInOut", 180 | name: "elasticInOut", 181 | ), 182 | ]; 183 | 184 | final otherCurves = [ 185 | CurveModel( 186 | curve: Curves.linear, 187 | code: "Curves.linear", 188 | name: "linear", 189 | ), 190 | CurveModel( 191 | curve: Curves.decelerate, 192 | code: "Curves.decelerate", 193 | name: "decelerate", 194 | ), 195 | CurveModel( 196 | curve: Curves.slowMiddle, 197 | code: "Curves.slowMiddle", 198 | name: "slowMiddle", 199 | ), 200 | CurveModel( 201 | curve: Curves.fastOutSlowIn, 202 | code: "Curves.fastOutSlowIn", 203 | name: "fastOutSlowIn", 204 | ), 205 | CurveModel( 206 | curve: Curves.fastEaseInToSlowEaseOut, 207 | code: "Curves.fastEaseInToSlowEaseOut", 208 | name: "fastEaseInToSlowEaseOut", 209 | ), 210 | CurveModel( 211 | curve: Curves.fastLinearToSlowEaseIn, 212 | code: "Curves.fastLinearToSlowEaseIn", 213 | name: "fastLinearToSlowEaseIn", 214 | ), 215 | // CurveModel( 216 | // curve: Cubic(.5, 0, .5, 1), 217 | // code: "Cubic(.5, 0, .5, 1)", 218 | // name: "custom", 219 | // isCustom: true, 220 | // ), 221 | ]; 222 | -------------------------------------------------------------------------------- /lib/utils/extension/string.dart: -------------------------------------------------------------------------------- 1 | extension StringExtension on String { 2 | String capitalizeFirst() => "${this[0].toUpperCase()}${substring(1)}"; 3 | } 4 | -------------------------------------------------------------------------------- /lib/utils/theme/colors.dart: -------------------------------------------------------------------------------- 1 | import 'dart:ui'; 2 | 3 | class ThemeColors { 4 | static const primaryColor = Color(0xfffca311); 5 | static const secondaryColor = Color(0xff1f0318); 6 | } 7 | -------------------------------------------------------------------------------- /lib/utils/theme/theme.dart: -------------------------------------------------------------------------------- 1 | import "package:flutter/material.dart"; 2 | import "package:flutter_curve_visualizer/utils/theme/colors.dart"; 3 | 4 | class MaterialTheme { 5 | final TextTheme textTheme; 6 | 7 | const MaterialTheme(this.textTheme); 8 | 9 | static ColorScheme lightScheme() { 10 | return const ColorScheme( 11 | brightness: Brightness.light, 12 | primary: Color(0xff825513), 13 | surfaceTint: Color(0xff825513), 14 | onPrimary: Color(0xffffffff), 15 | primaryContainer: Color(0xffffddb8), 16 | onPrimaryContainer: Color(0xff2a1700), 17 | secondary: Color(0xff196584), 18 | onSecondary: Color(0xffffffff), 19 | secondaryContainer: Color(0xffc1e8ff), 20 | onSecondaryContainer: Color(0xff001e2b), 21 | tertiary: Color(0xff7d570e), 22 | onTertiary: Color(0xffffffff), 23 | tertiaryContainer: Color(0xffffdeae), 24 | onTertiaryContainer: Color(0xff281800), 25 | error: Color(0xffba1a1a), 26 | onError: Color(0xffffffff), 27 | errorContainer: Color(0xffffdad6), 28 | onErrorContainer: Color(0xff410002), 29 | surface: Color(0xfffff8f4), 30 | onSurface: Color(0xff211a13), 31 | onSurfaceVariant: Color(0xff504539), 32 | outline: Color(0xff827568), 33 | outlineVariant: Color(0xffd4c4b5), 34 | shadow: Color(0xff000000), 35 | scrim: Color(0xff000000), 36 | inverseSurface: Color(0xff372f27), 37 | inversePrimary: Color(0xfff8bb71), 38 | primaryFixed: Color(0xffffddb8), 39 | onPrimaryFixed: Color(0xff2a1700), 40 | primaryFixedDim: Color(0xfff8bb71), 41 | onPrimaryFixedVariant: Color(0xff653e00), 42 | secondaryFixed: Color(0xffc1e8ff), 43 | onSecondaryFixed: Color(0xff001e2b), 44 | secondaryFixedDim: Color(0xff8ecff2), 45 | onSecondaryFixedVariant: Color(0xff004d67), 46 | tertiaryFixed: Color(0xffffdeae), 47 | onTertiaryFixed: Color(0xff281800), 48 | tertiaryFixedDim: Color(0xfff1be6d), 49 | onTertiaryFixedVariant: Color(0xff604100), 50 | surfaceDim: Color(0xffe5d8cc), 51 | surfaceBright: Color(0xfffff8f4), 52 | surfaceContainerLowest: Color(0xffffffff), 53 | surfaceContainerLow: Color(0xfffff1e5), 54 | surfaceContainer: Color(0xfff9ece0), 55 | surfaceContainerHigh: Color(0xfff3e6da), 56 | surfaceContainerHighest: Color(0xffeee0d4), 57 | ); 58 | } 59 | 60 | ThemeData light() { 61 | return theme(lightScheme()); 62 | } 63 | 64 | static ColorScheme lightMediumContrastScheme() { 65 | return const ColorScheme( 66 | brightness: Brightness.light, 67 | primary: Color(0xff603b00), 68 | surfaceTint: Color(0xff825513), 69 | onPrimary: Color(0xffffffff), 70 | primaryContainer: Color(0xff9c6b28), 71 | onPrimaryContainer: Color(0xffffffff), 72 | secondary: Color(0xff004862), 73 | onSecondary: Color(0xffffffff), 74 | secondaryContainer: Color(0xff377c9c), 75 | onSecondaryContainer: Color(0xffffffff), 76 | tertiary: Color(0xff5b3d00), 77 | onTertiary: Color(0xffffffff), 78 | tertiaryContainer: Color(0xff966d25), 79 | onTertiaryContainer: Color(0xffffffff), 80 | error: Color(0xff8c0009), 81 | onError: Color(0xffffffff), 82 | errorContainer: Color(0xffda342e), 83 | onErrorContainer: Color(0xffffffff), 84 | surface: Color(0xfffff8f4), 85 | onSurface: Color(0xff211a13), 86 | onSurfaceVariant: Color(0xff4c4136), 87 | outline: Color(0xff695d51), 88 | outlineVariant: Color(0xff86786b), 89 | shadow: Color(0xff000000), 90 | scrim: Color(0xff000000), 91 | inverseSurface: Color(0xff372f27), 92 | inversePrimary: Color(0xfff8bb71), 93 | primaryFixed: Color(0xff9c6b28), 94 | onPrimaryFixed: Color(0xffffffff), 95 | primaryFixedDim: Color(0xff7f5210), 96 | onPrimaryFixedVariant: Color(0xffffffff), 97 | secondaryFixed: Color(0xff377c9c), 98 | onSecondaryFixed: Color(0xffffffff), 99 | secondaryFixedDim: Color(0xff146382), 100 | onSecondaryFixedVariant: Color(0xffffffff), 101 | tertiaryFixed: Color(0xff966d25), 102 | onTertiaryFixed: Color(0xffffffff), 103 | tertiaryFixedDim: Color(0xff7b550b), 104 | onTertiaryFixedVariant: Color(0xffffffff), 105 | surfaceDim: Color(0xffe5d8cc), 106 | surfaceBright: Color(0xfffff8f4), 107 | surfaceContainerLowest: Color(0xffffffff), 108 | surfaceContainerLow: Color(0xfffff1e5), 109 | surfaceContainer: Color(0xfff9ece0), 110 | surfaceContainerHigh: Color(0xfff3e6da), 111 | surfaceContainerHighest: Color(0xffeee0d4), 112 | ); 113 | } 114 | 115 | ThemeData lightMediumContrast() { 116 | return theme(lightMediumContrastScheme()); 117 | } 118 | 119 | static ColorScheme lightHighContrastScheme() { 120 | return const ColorScheme( 121 | brightness: Brightness.light, 122 | primary: Color(0xff331d00), 123 | surfaceTint: Color(0xff825513), 124 | onPrimary: Color(0xffffffff), 125 | primaryContainer: Color(0xff603b00), 126 | onPrimaryContainer: Color(0xffffffff), 127 | secondary: Color(0xff002635), 128 | onSecondary: Color(0xffffffff), 129 | secondaryContainer: Color(0xff004862), 130 | onSecondaryContainer: Color(0xffffffff), 131 | tertiary: Color(0xff311f00), 132 | onTertiary: Color(0xffffffff), 133 | tertiaryContainer: Color(0xff5b3d00), 134 | onTertiaryContainer: Color(0xffffffff), 135 | error: Color(0xff4e0002), 136 | onError: Color(0xffffffff), 137 | errorContainer: Color(0xff8c0009), 138 | onErrorContainer: Color(0xffffffff), 139 | surface: Color(0xfffff8f4), 140 | onSurface: Color(0xff000000), 141 | onSurfaceVariant: Color(0xff2b2218), 142 | outline: Color(0xff4c4136), 143 | outlineVariant: Color(0xff4c4136), 144 | shadow: Color(0xff000000), 145 | scrim: Color(0xff000000), 146 | inverseSurface: Color(0xff372f27), 147 | inversePrimary: Color(0xffffe8d2), 148 | primaryFixed: Color(0xff603b00), 149 | onPrimaryFixed: Color(0xffffffff), 150 | primaryFixedDim: Color(0xff422700), 151 | onPrimaryFixedVariant: Color(0xffffffff), 152 | secondaryFixed: Color(0xff004862), 153 | onSecondaryFixed: Color(0xffffffff), 154 | secondaryFixedDim: Color(0xff003143), 155 | onSecondaryFixedVariant: Color(0xffffffff), 156 | tertiaryFixed: Color(0xff5b3d00), 157 | onTertiaryFixed: Color(0xffffffff), 158 | tertiaryFixedDim: Color(0xff3e2800), 159 | onTertiaryFixedVariant: Color(0xffffffff), 160 | surfaceDim: Color(0xffe5d8cc), 161 | surfaceBright: Color(0xfffff8f4), 162 | surfaceContainerLowest: Color(0xffffffff), 163 | surfaceContainerLow: Color(0xfffff1e5), 164 | surfaceContainer: Color(0xfff9ece0), 165 | surfaceContainerHigh: Color(0xfff3e6da), 166 | surfaceContainerHighest: Color(0xffeee0d4), 167 | ); 168 | } 169 | 170 | ThemeData lightHighContrast() { 171 | return theme(lightHighContrastScheme()); 172 | } 173 | 174 | static ColorScheme darkScheme() { 175 | return const ColorScheme( 176 | brightness: Brightness.dark, 177 | primary: Color(0xfff8bb71), 178 | surfaceTint: Color(0xfff8bb71), 179 | onPrimary: Color(0xff472a00), 180 | primaryContainer: Color(0xff653e00), 181 | onPrimaryContainer: Color(0xffffddb8), 182 | secondary: Color(0xff8ecff2), 183 | onSecondary: Color(0xff003548), 184 | secondaryContainer: Color(0xff004d67), 185 | onSecondaryContainer: Color(0xffc1e8ff), 186 | tertiary: Color(0xfff1be6d), 187 | onTertiary: Color(0xff432c00), 188 | tertiaryContainer: Color(0xff604100), 189 | onTertiaryContainer: Color(0xffffdeae), 190 | error: Color(0xffffb4ab), 191 | onError: Color(0xff690005), 192 | errorContainer: Color(0xff93000a), 193 | onErrorContainer: Color(0xffffdad6), 194 | surface: Color(0xff18120c), 195 | onSurface: Color(0xffeee0d4), 196 | onSurfaceVariant: Color(0xffd4c4b5), 197 | outline: Color(0xff9c8e80), 198 | outlineVariant: Color(0xff504539), 199 | shadow: Color(0xff000000), 200 | scrim: Color(0xff000000), 201 | inverseSurface: Color(0xffeee0d4), 202 | inversePrimary: Color(0xff825513), 203 | primaryFixed: Color(0xffffddb8), 204 | onPrimaryFixed: Color(0xff2a1700), 205 | primaryFixedDim: Color(0xfff8bb71), 206 | onPrimaryFixedVariant: Color(0xff653e00), 207 | secondaryFixed: Color(0xffc1e8ff), 208 | onSecondaryFixed: Color(0xff001e2b), 209 | secondaryFixedDim: Color(0xff8ecff2), 210 | onSecondaryFixedVariant: Color(0xff004d67), 211 | tertiaryFixed: Color(0xffffdeae), 212 | onTertiaryFixed: Color(0xff281800), 213 | tertiaryFixedDim: Color(0xfff1be6d), 214 | onTertiaryFixedVariant: Color(0xff604100), 215 | surfaceDim: Color(0xff18120c), 216 | surfaceBright: Color(0xff403830), 217 | surfaceContainerLowest: Color(0xff130d07), 218 | surfaceContainerLow: Color(0xff211a13), 219 | surfaceContainer: Color(0xff251e17), 220 | surfaceContainerHigh: Color(0xff302921), 221 | surfaceContainerHighest: Color(0xff3b332b), 222 | ); 223 | } 224 | 225 | ThemeData dark() { 226 | return theme(darkScheme()); 227 | } 228 | 229 | static ColorScheme darkMediumContrastScheme() { 230 | return const ColorScheme( 231 | brightness: Brightness.dark, 232 | primary: Color(0xfffcbf74), 233 | surfaceTint: Color(0xfff8bb71), 234 | onPrimary: Color(0xff231300), 235 | primaryContainer: Color(0xffbc8641), 236 | onPrimaryContainer: Color(0xff000000), 237 | secondary: Color(0xff92d3f6), 238 | onSecondary: Color(0xff001924), 239 | secondaryContainer: Color(0xff5698b9), 240 | onSecondaryContainer: Color(0xff000000), 241 | tertiary: Color(0xfff6c271), 242 | onTertiary: Color(0xff211400), 243 | tertiaryContainer: Color(0xffb6893e), 244 | onTertiaryContainer: Color(0xff000000), 245 | error: Color(0xffffbab1), 246 | onError: Color(0xff370001), 247 | errorContainer: Color(0xffff5449), 248 | onErrorContainer: Color(0xff000000), 249 | surface: Color(0xff18120c), 250 | onSurface: Color(0xfffffaf7), 251 | onSurfaceVariant: Color(0xffd8c8b9), 252 | outline: Color(0xffafa092), 253 | outlineVariant: Color(0xff8e8173), 254 | shadow: Color(0xff000000), 255 | scrim: Color(0xff000000), 256 | inverseSurface: Color(0xffeee0d4), 257 | inversePrimary: Color(0xff673f00), 258 | primaryFixed: Color(0xffffddb8), 259 | onPrimaryFixed: Color(0xff1c0e00), 260 | primaryFixedDim: Color(0xfff8bb71), 261 | onPrimaryFixedVariant: Color(0xff4f2f00), 262 | secondaryFixed: Color(0xffc1e8ff), 263 | onSecondaryFixed: Color(0xff00131d), 264 | secondaryFixedDim: Color(0xff8ecff2), 265 | onSecondaryFixedVariant: Color(0xff003b50), 266 | tertiaryFixed: Color(0xffffdeae), 267 | onTertiaryFixed: Color(0xff1b0f00), 268 | tertiaryFixedDim: Color(0xfff1be6d), 269 | onTertiaryFixedVariant: Color(0xff4b3100), 270 | surfaceDim: Color(0xff18120c), 271 | surfaceBright: Color(0xff403830), 272 | surfaceContainerLowest: Color(0xff130d07), 273 | surfaceContainerLow: Color(0xff211a13), 274 | surfaceContainer: Color(0xff251e17), 275 | surfaceContainerHigh: Color(0xff302921), 276 | surfaceContainerHighest: Color(0xff3b332b), 277 | ); 278 | } 279 | 280 | ThemeData darkMediumContrast() { 281 | return theme(darkMediumContrastScheme()); 282 | } 283 | 284 | static ColorScheme darkHighContrastScheme() { 285 | return const ColorScheme( 286 | brightness: Brightness.dark, 287 | primary: Color(0xfffffaf7), 288 | surfaceTint: Color(0xfff8bb71), 289 | onPrimary: Color(0xff000000), 290 | primaryContainer: Color(0xfffcbf74), 291 | onPrimaryContainer: Color(0xff000000), 292 | secondary: Color(0xfff7fbff), 293 | onSecondary: Color(0xff000000), 294 | secondaryContainer: Color(0xff92d3f6), 295 | onSecondaryContainer: Color(0xff000000), 296 | tertiary: Color(0xfffffaf7), 297 | onTertiary: Color(0xff000000), 298 | tertiaryContainer: Color(0xfff6c271), 299 | onTertiaryContainer: Color(0xff000000), 300 | error: Color(0xfffff9f9), 301 | onError: Color(0xff000000), 302 | errorContainer: Color(0xffffbab1), 303 | onErrorContainer: Color(0xff000000), 304 | surface: Color(0xff18120c), 305 | onSurface: Color(0xffffffff), 306 | onSurfaceVariant: Color(0xfffffaf7), 307 | outline: Color(0xffd8c8b9), 308 | outlineVariant: Color(0xffd8c8b9), 309 | shadow: Color(0xff000000), 310 | scrim: Color(0xff000000), 311 | inverseSurface: Color(0xffeee0d4), 312 | inversePrimary: Color(0xff3e2400), 313 | primaryFixed: Color(0xffffe2c4), 314 | onPrimaryFixed: Color(0xff000000), 315 | primaryFixedDim: Color(0xfffcbf74), 316 | onPrimaryFixedVariant: Color(0xff231300), 317 | secondaryFixed: Color(0xffccebff), 318 | onSecondaryFixed: Color(0xff000000), 319 | secondaryFixedDim: Color(0xff92d3f6), 320 | onSecondaryFixedVariant: Color(0xff001924), 321 | tertiaryFixed: Color(0xffffe3bc), 322 | onTertiaryFixed: Color(0xff000000), 323 | tertiaryFixedDim: Color(0xfff6c271), 324 | onTertiaryFixedVariant: Color(0xff211400), 325 | surfaceDim: Color(0xff18120c), 326 | surfaceBright: Color(0xff403830), 327 | surfaceContainerLowest: Color(0xff130d07), 328 | surfaceContainerLow: Color(0xff211a13), 329 | surfaceContainer: Color(0xff251e17), 330 | surfaceContainerHigh: Color(0xff302921), 331 | surfaceContainerHighest: Color(0xff3b332b), 332 | ); 333 | } 334 | 335 | ThemeData darkHighContrast() { 336 | return theme(darkHighContrastScheme()); 337 | } 338 | 339 | ThemeData theme(ColorScheme colorScheme) => ThemeData( 340 | primaryColor: ThemeColors.primaryColor, 341 | useMaterial3: true, 342 | brightness: colorScheme.brightness, 343 | colorScheme: colorScheme, 344 | snackBarTheme: SnackBarThemeData(showCloseIcon: true), 345 | textTheme: textTheme.apply( 346 | fontFamily: "Outfit", 347 | bodyColor: colorScheme.onSurface, 348 | displayColor: colorScheme.onSurface, 349 | ), 350 | scaffoldBackgroundColor: colorScheme.surface, 351 | canvasColor: colorScheme.surface, 352 | ); 353 | 354 | List get extendedColors => []; 355 | } 356 | 357 | class ExtendedColor { 358 | final Color seed, value; 359 | final ColorFamily light; 360 | final ColorFamily lightHighContrast; 361 | final ColorFamily lightMediumContrast; 362 | final ColorFamily dark; 363 | final ColorFamily darkHighContrast; 364 | final ColorFamily darkMediumContrast; 365 | 366 | const ExtendedColor({ 367 | required this.seed, 368 | required this.value, 369 | required this.light, 370 | required this.lightHighContrast, 371 | required this.lightMediumContrast, 372 | required this.dark, 373 | required this.darkHighContrast, 374 | required this.darkMediumContrast, 375 | }); 376 | } 377 | 378 | class ColorFamily { 379 | const ColorFamily({ 380 | required this.color, 381 | required this.onColor, 382 | required this.colorContainer, 383 | required this.onColorContainer, 384 | }); 385 | 386 | final Color color; 387 | final Color onColor; 388 | final Color colorContainer; 389 | final Color onColorContainer; 390 | } 391 | -------------------------------------------------------------------------------- /lib/utils/theme/theme_provider.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:shared_preferences/shared_preferences.dart'; 3 | 4 | class ThemeProvider extends ChangeNotifier { 5 | final SharedPreferences pref; 6 | 7 | ThemeProvider({required this.pref}); 8 | 9 | ThemeMode getThemeMode() { 10 | final isDarkMode = pref.getBool('darkMode') ?? false; 11 | return isDarkMode ? ThemeMode.dark : ThemeMode.light; 12 | } 13 | 14 | Future toggleTheme() async { 15 | final isDarkMode = pref.getBool('darkMode') ?? false; 16 | pref.setBool('darkMode', !isDarkMode); 17 | notifyListeners(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /lib/views/home_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_animate/flutter_animate.dart'; 3 | import 'package:flutter_curve_visualizer/model/curve_model.dart'; 4 | import 'package:flutter_curve_visualizer/views/widgets/screen_mode.dart'; 5 | import 'package:flutter_curve_visualizer/views/widgets/appbar.dart'; 6 | import 'package:flutter_curve_visualizer/views/widgets/dropdown_menu.dart'; 7 | import 'package:flutter_curve_visualizer/views/widgets/graph/graph_widget.dart'; 8 | import 'package:flutter_curve_visualizer/views/widgets/time_slider.dart'; 9 | import 'package:provider/provider.dart'; 10 | import 'widgets/animated_box/animated_boxes.dart'; 11 | import 'widgets/animated_box/provider.dart'; 12 | import 'widgets/code_block.dart'; 13 | 14 | class HomePage extends StatefulWidget { 15 | const HomePage({super.key}); 16 | 17 | @override 18 | State createState() => _HomePageState(); 19 | } 20 | 21 | class _HomePageState extends State with TickerProviderStateMixin { 22 | late AnimationController playPauseController; 23 | late AnimationController controller; 24 | late CurvedAnimation curveAnimation; 25 | 26 | late String selectedCategory; 27 | late CurveModel selectedCurve; 28 | 29 | late int animationTime; 30 | 31 | @override 32 | void initState() { 33 | super.initState(); 34 | 35 | selectedCategory = CurveModel.list.keys.first; 36 | selectedCurve = CurveModel.list.values.first.first; 37 | 38 | animationTime = 2; 39 | 40 | playPauseController = AnimationController( 41 | vsync: this, 42 | duration: 100.ms, 43 | reverseDuration: 100.ms, 44 | )..forward(); 45 | 46 | controller = AnimationController( 47 | vsync: this, 48 | duration: Duration(seconds: animationTime), 49 | )..repeat(reverse: true); 50 | 51 | curveAnimation = CurvedAnimation( 52 | parent: controller, 53 | curve: selectedCurve.curve, 54 | ); 55 | } 56 | 57 | @override 58 | void dispose() { 59 | playPauseController.dispose(); 60 | controller.dispose(); 61 | super.dispose(); 62 | } 63 | 64 | void updateCategory(String? category) { 65 | if (category == null) return; 66 | 67 | setState(() { 68 | selectedCategory = category; 69 | selectedCurve = CurveModel.list[category]!.first; 70 | curveAnimation.curve = selectedCurve.curve; 71 | }); 72 | } 73 | 74 | void updateCurve(CurveModel? curve) { 75 | if (curve == null || curve == selectedCurve) return; 76 | 77 | setState(() { 78 | selectedCurve = curve; 79 | curveAnimation.curve = curve.curve; 80 | }); 81 | } 82 | 83 | void updateAnimationTime(double seconds) { 84 | // return if both values are same 85 | if (animationTime == seconds.toInt()) return; 86 | 87 | setState(() { 88 | animationTime = seconds.toInt(); 89 | 90 | controller.duration = Duration(seconds: animationTime); 91 | 92 | curveAnimation.curve = selectedCurve.curve; 93 | 94 | if (controller.isForwardOrCompleted) { 95 | controller.repeat(reverse: true); 96 | } else { 97 | controller.reverse().then((value) => controller.repeat(reverse: true)); 98 | } 99 | 100 | playPauseController.forward(); 101 | }); 102 | } 103 | 104 | void playPauseAnimation() { 105 | if (controller.isAnimating) { 106 | controller.stop(); 107 | playPauseController.reverse(); 108 | } else { 109 | if (controller.isForwardOrCompleted) { 110 | controller.repeat(reverse: true); 111 | } else { 112 | controller.reverse().then((value) => controller.repeat(reverse: true)); 113 | } 114 | playPauseController.forward(); 115 | } 116 | } 117 | 118 | @override 119 | Widget build(BuildContext context) { 120 | final screenMode = ScreenModeWidget.of(context); 121 | 122 | final double spacing = screenMode.spacing; 123 | 124 | final animationWidget = Column( 125 | spacing: spacing, 126 | mainAxisAlignment: MainAxisAlignment.start, 127 | mainAxisSize: MainAxisSize.min, 128 | children: [ 129 | // Graph 130 | GraphWidget( 131 | controller: controller, 132 | animation: curveAnimation, 133 | ), 134 | 135 | // Box Animations 136 | Consumer( 137 | builder: (context, value, child) { 138 | final list = value.animationBoxReordableList; 139 | 140 | return AnimationBoxes( 141 | curveAnimation: curveAnimation, 142 | animationTypes: list, 143 | onAcceptWithDetails: (details, item) { 144 | final oldIndex = list.indexOf(details.data); 145 | final newIndex = list.indexOf(item); 146 | 147 | if (oldIndex != -1 && newIndex != -1) { 148 | list.removeAt(oldIndex); 149 | list.insert(newIndex, details.data); 150 | value.saveList(list); 151 | } 152 | }, 153 | ); 154 | }, 155 | ), 156 | SizedBox(height: spacing * 2), 157 | ], 158 | ); 159 | 160 | final controlsWidget = ConstrainedBox( 161 | constraints: BoxConstraints( 162 | maxWidth: 400, 163 | ), 164 | child: Column( 165 | mainAxisSize: MainAxisSize.min, 166 | mainAxisAlignment: MainAxisAlignment.end, 167 | spacing: spacing, 168 | children: [ 169 | // Code block 170 | CodeBlock(code: selectedCurve.code), 171 | 172 | // Curve selector 173 | Row( 174 | spacing: 10, 175 | mainAxisSize: MainAxisSize.min, 176 | mainAxisAlignment: MainAxisAlignment.start, 177 | children: [ 178 | // Curve category 179 | Flexible( 180 | child: DropdownMenuWidget( 181 | title: "Category", 182 | value: selectedCategory, 183 | items: CurveModel.list.keys.toList(), 184 | onChanged: updateCategory, 185 | ), 186 | ), 187 | 188 | // Curve type 189 | Flexible( 190 | flex: 2, 191 | child: DropdownMenuWidget( 192 | title: "Curve", 193 | value: selectedCurve, 194 | items: CurveModel.list[selectedCategory]!.toList(), 195 | onChanged: updateCurve, 196 | builder: (context, value, textStyle) { 197 | return Text(value.name.toString(), style: textStyle); 198 | }, 199 | ), 200 | ), 201 | ], 202 | ), 203 | 204 | // Animation time 205 | TimeSlider( 206 | animationTime: animationTime, 207 | onChanged: updateAnimationTime, 208 | ), 209 | ], 210 | ), 211 | ); 212 | 213 | return Scaffold( 214 | appBar: HomeAppBar(), 215 | floatingActionButton: FloatingActionButton( 216 | onPressed: playPauseAnimation, 217 | child: Padding( 218 | padding: const EdgeInsets.all(8.0), 219 | child: AnimatedIcon( 220 | icon: AnimatedIcons.play_pause, 221 | progress: playPauseController, 222 | ), 223 | ), 224 | ), 225 | body: Container( 226 | alignment: Alignment.center, 227 | padding: const EdgeInsets.symmetric(horizontal: 16.0), 228 | width: double.infinity, 229 | child: SingleChildScrollView( 230 | child: switch (ScreenModeWidget.of(context)) { 231 | ScreenMode.mobile => Column( 232 | spacing: spacing, 233 | children: [ 234 | animationWidget, 235 | controlsWidget, 236 | SizedBox(height: spacing), 237 | ], 238 | ), 239 | ScreenMode.tablet => Column( 240 | spacing: spacing, 241 | children: [ 242 | animationWidget, 243 | controlsWidget, 244 | SizedBox(height: spacing), 245 | ], 246 | ), 247 | ScreenMode.web => Row( 248 | spacing: spacing, 249 | mainAxisSize: MainAxisSize.min, 250 | children: [ 251 | Expanded( 252 | flex: 2, 253 | child: animationWidget, 254 | ), 255 | Flexible( 256 | flex: 1, 257 | child: controlsWidget, 258 | ), 259 | SizedBox(width: spacing * 3), 260 | ], 261 | ), 262 | }, 263 | ), 264 | ), 265 | ); 266 | } 267 | } 268 | -------------------------------------------------------------------------------- /lib/views/widgets/animated_box/animated_box_widget.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | import 'package:flutter/material.dart'; 3 | 4 | enum AnimationType { translateX, translateY, rotate, scale, fade, flip } 5 | 6 | class AnimatedBoxWidget extends StatelessWidget { 7 | final AnimationType animationType; 8 | final Animation animation; 9 | 10 | const AnimatedBoxWidget({ 11 | super.key, 12 | required this.animation, 13 | required this.animationType, 14 | }); 15 | 16 | @override 17 | Widget build(BuildContext context) { 18 | final theme = Theme.of(context).colorScheme; 19 | 20 | return LayoutBuilder(builder: (context, constraints) { 21 | final boxSize = min(constraints.maxWidth, constraints.maxHeight) / 4; 22 | 23 | return AnimatedBuilder( 24 | animation: animation, 25 | child: ColoredBox( 26 | color: theme.primary, 27 | child: SizedBox.square(dimension: boxSize), 28 | ), 29 | builder: (context, child) { 30 | return PhysicalModel( 31 | color: Colors.transparent, 32 | shadowColor: theme.shadow, 33 | elevation: 1.0, 34 | child: ColoredBox( 35 | color: theme.onPrimaryFixed, 36 | child: Align( 37 | alignment: Alignment.center, 38 | child: switch (animationType) { 39 | /// Translate x 40 | AnimationType.translateX => 41 | _buildTransformX(constraints, boxSize, child), 42 | 43 | /// Translate Y 44 | AnimationType.translateY => 45 | _buildTransformY(constraints, boxSize, child), 46 | 47 | /// Rotate 48 | AnimationType.rotate => _buildRotate(child), 49 | 50 | /// Scale 51 | AnimationType.scale => _buildScale(child), 52 | 53 | /// Fade 54 | AnimationType.fade => _buildFade(child), 55 | 56 | /// Flip 57 | AnimationType.flip => _buildFlip(child), 58 | }, 59 | ), 60 | ), 61 | ); 62 | }, 63 | ); 64 | }); 65 | } 66 | 67 | Transform _buildFlip(Widget? child) { 68 | return Transform( 69 | alignment: FractionalOffset.center, 70 | transform: Matrix4.identity() 71 | ..setEntry(2, 1, 0.0002) 72 | ..rotateY(animation.value * pi), 73 | child: child, 74 | ); 75 | } 76 | 77 | AnimatedOpacity _buildFade(Widget? child) { 78 | return AnimatedOpacity( 79 | duration: const Duration(milliseconds: 100), 80 | opacity: animation.value.clamp(0.0, 1.0), 81 | child: child, 82 | ); 83 | } 84 | 85 | Transform _buildTransformY( 86 | BoxConstraints constraints, double boxSize, Widget? child) { 87 | return Transform.translate( 88 | offset: Tween( 89 | begin: Offset(0, (-constraints.maxHeight + boxSize) / 2), 90 | end: Offset(0, (constraints.maxHeight - boxSize) / 2), 91 | ).transform(animation.value), 92 | child: child, 93 | ); 94 | } 95 | 96 | Transform _buildTransformX( 97 | BoxConstraints constraints, double boxSize, Widget? child) { 98 | return Transform.translate( 99 | offset: Tween( 100 | begin: Offset((-constraints.maxWidth + boxSize) / 2, 0), 101 | end: Offset((constraints.maxHeight - boxSize) / 2, 0), 102 | ).transform(animation.value), 103 | child: child, 104 | ); 105 | } 106 | 107 | Transform _buildScale(Widget? child) { 108 | return Transform.scale( 109 | scale: animation.value, 110 | child: child, 111 | ); 112 | } 113 | 114 | Transform _buildRotate(Widget? child) => 115 | Transform.rotate(angle: animation.value * pi, child: child); 116 | } 117 | -------------------------------------------------------------------------------- /lib/views/widgets/animated_box/animated_boxes.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_animate/flutter_animate.dart'; 3 | import 'package:flutter_curve_visualizer/utils/extension/string.dart'; 4 | import 'package:flutter_curve_visualizer/views/widgets/screen_mode.dart'; 5 | import 'animated_box_widget.dart'; 6 | 7 | class AnimationBoxes extends StatelessWidget { 8 | const AnimationBoxes({ 9 | super.key, 10 | required this.curveAnimation, 11 | required this.animationTypes, 12 | required this.onAcceptWithDetails, 13 | }); 14 | 15 | final List animationTypes; 16 | final CurvedAnimation curveAnimation; 17 | final void Function(DragTargetDetails details, int newIndex)? 18 | onAcceptWithDetails; 19 | 20 | @override 21 | Widget build(BuildContext context) { 22 | final screenMode = ScreenModeWidget.of(context); 23 | 24 | final spacing = screenMode.spacing; 25 | 26 | const boxSize = 100.0; 27 | 28 | return SizedBox( 29 | width: MediaQuery.of(context).size.width / 30 | (screenMode.isMobileOrTablet ? 1 : 2), 31 | child: Wrap( 32 | key: ValueKey(animationTypes), 33 | spacing: spacing / 2, 34 | runSpacing: spacing / 2, 35 | runAlignment: WrapAlignment.center, 36 | alignment: WrapAlignment.center, 37 | children: animationTypes.map( 38 | (index) { 39 | return DragTarget( 40 | onWillAcceptWithDetails: (_) => true, 41 | onAcceptWithDetails: (details) => 42 | onAcceptWithDetails?.call(details, index), 43 | builder: (_, __, ___) { 44 | final child = Column( 45 | mainAxisAlignment: MainAxisAlignment.start, 46 | spacing: 8.0, 47 | children: [ 48 | Text(AnimationType.values[index].name.capitalizeFirst()), 49 | SizedBox.square( 50 | dimension: boxSize, 51 | child: AnimatedBoxWidget( 52 | animationType: AnimationType.values[index], 53 | animation: curveAnimation, 54 | ), 55 | ), 56 | ], 57 | ); 58 | 59 | return MouseRegion( 60 | cursor: SystemMouseCursors.grab, 61 | child: Draggable( 62 | data: index, 63 | childWhenDragging: Column( 64 | children: [ 65 | Text(""), 66 | SizedBox.square(dimension: boxSize), 67 | ], 68 | ), 69 | feedback: AnimatedScale( 70 | scale: 1.150, 71 | duration: 200.ms, 72 | child: Material( 73 | child: child, 74 | ), 75 | ), 76 | child: child, 77 | ), 78 | ); 79 | }, 80 | ); 81 | }, 82 | ).toList(), 83 | ), 84 | ); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /lib/views/widgets/animated_box/provider.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:shared_preferences/shared_preferences.dart'; 3 | 4 | import 'animated_box_widget.dart'; 5 | 6 | const key = 'animationBoxReordableList'; 7 | 8 | class AnimatedBoxesProvider extends ChangeNotifier { 9 | final SharedPreferences pref; 10 | 11 | AnimatedBoxesProvider({required this.pref}) { 12 | getList(); 13 | } 14 | 15 | late List animationBoxReordableList; 16 | 17 | void getList() { 18 | final list = pref.getStringList(key); 19 | 20 | if (list != null && list.isNotEmpty) { 21 | animationBoxReordableList = list.map((e) => int.parse(e)).toList(); 22 | } else { 23 | animationBoxReordableList = 24 | List.generate(AnimationType.values.length, (index) => index); 25 | } 26 | 27 | notifyListeners(); 28 | } 29 | 30 | void saveList(List list) { 31 | pref.setStringList(key, list.map((e) => e.toString()).toList()); 32 | animationBoxReordableList = list; 33 | notifyListeners(); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /lib/views/widgets/appbar.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_animate/flutter_animate.dart'; 3 | import 'package:flutter_curve_visualizer/utils/theme/theme_provider.dart'; 4 | import 'package:flutter_curve_visualizer/views/widgets/screen_mode.dart'; 5 | import 'package:flutter_svg/flutter_svg.dart'; 6 | import 'package:light_dark_theme_toggle/light_dark_theme_toggle.dart'; 7 | import 'package:provider/provider.dart'; 8 | import 'package:url_launcher/url_launcher.dart'; 9 | 10 | class HomeAppBar extends StatelessWidget implements PreferredSizeWidget { 11 | const HomeAppBar({super.key}); 12 | 13 | @override 14 | Widget build(BuildContext context) { 15 | final theme = Theme.of(context); 16 | 17 | final screenMode = ScreenModeWidget.of(context); 18 | 19 | final actionPadding = const EdgeInsets.only(right: 12.0); 20 | 21 | final width = MediaQuery.sizeOf(context).width; 22 | 23 | double hPadding = switch (screenMode) { 24 | ScreenMode.mobile => 0, 25 | ScreenMode.tablet => 0, 26 | ScreenMode.web => width * 0.075, 27 | }; 28 | 29 | return Padding( 30 | padding: EdgeInsets.symmetric(horizontal: hPadding), 31 | child: AppBar( 32 | title: const Text('Flutter Curve Visualizer'), 33 | actions: [ 34 | Padding( 35 | padding: actionPadding, 36 | child: IconButton( 37 | onPressed: () { 38 | launchUrl( 39 | Uri.parse( 40 | "https://github.com/vchib1/flutter-curve-visualizer", 41 | ), 42 | ); 43 | }, 44 | icon: SvgPicture.asset( 45 | "assets/svg/github.svg", 46 | width: theme.iconTheme.size ?? 24, 47 | height: theme.iconTheme.size ?? 24, 48 | colorFilter: ColorFilter.mode( 49 | theme.iconTheme.color ?? Colors.black, 50 | BlendMode.srcIn, 51 | ), 52 | ), 53 | ), 54 | ), 55 | Padding( 56 | padding: actionPadding, 57 | child: Consumer( 58 | builder: (context, value, child) { 59 | return LightDarkThemeToggle( 60 | themeIconType: ThemeIconType.expand, 61 | duration: 350.milliseconds, 62 | reverseDuration: 350.milliseconds, 63 | value: value.getThemeMode() == ThemeMode.dark, 64 | onChanged: (isDark) { 65 | value.toggleTheme(); 66 | }, 67 | ); 68 | }, 69 | ), 70 | ), 71 | ], 72 | ), 73 | ); 74 | } 75 | 76 | @override 77 | Size get preferredSize => const Size.fromHeight(kToolbarHeight); 78 | } 79 | -------------------------------------------------------------------------------- /lib/views/widgets/code_block.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/services.dart'; 3 | import 'package:flutter_curve_visualizer/views/widgets/screen_mode.dart'; 4 | 5 | class CodeBlock extends StatelessWidget { 6 | final String code; 7 | 8 | const CodeBlock({super.key, required this.code}); 9 | 10 | void copyCode(BuildContext context, String code) { 11 | Clipboard.setData(ClipboardData(text: code)); 12 | 13 | if (!context.mounted) return; 14 | 15 | ScaffoldMessenger.of(context) 16 | ..clearSnackBars() 17 | ..showSnackBar(const SnackBar(content: Text('Copied to clipboard'))); 18 | } 19 | 20 | @override 21 | Widget build(BuildContext context) { 22 | final mode = ScreenModeWidget.of(context); 23 | 24 | final codeStyle = switch (mode) { 25 | ScreenMode.mobile => Theme.of(context).textTheme.bodyMedium, 26 | ScreenMode.tablet => Theme.of(context).textTheme.bodyLarge, 27 | ScreenMode.web => Theme.of(context).textTheme.bodyLarge, 28 | }; 29 | 30 | final double spacing = mode.isMobile ? 5 : 10; 31 | 32 | final double radius = mode.isMobile ? 3 : 5; 33 | 34 | final lightsColors = [ 35 | Color(0xffff5d5e), 36 | Color(0xfffbbe27), 37 | Color(0xff2bc542), 38 | ]; 39 | 40 | final borderRadius = BorderRadius.circular(10); 41 | 42 | return PhysicalModel( 43 | color: Colors.transparent, 44 | shadowColor: Theme.of(context).colorScheme.shadow, 45 | elevation: 1.0, 46 | borderRadius: borderRadius, 47 | child: Container( 48 | padding: EdgeInsets.all(8.0), 49 | decoration: BoxDecoration( 50 | color: Theme.of(context).colorScheme.onPrimaryFixed, 51 | borderRadius: borderRadius, 52 | ), 53 | child: Column( 54 | crossAxisAlignment: CrossAxisAlignment.start, 55 | mainAxisSize: MainAxisSize.min, 56 | spacing: spacing, 57 | children: [ 58 | Align( 59 | alignment: Alignment.topCenter, 60 | child: Row( 61 | mainAxisSize: MainAxisSize.min, 62 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 63 | children: [ 64 | Wrap( 65 | spacing: 5, 66 | children: List.from(lightsColors).map((color) { 67 | return CircleAvatar( 68 | radius: radius, backgroundColor: color); 69 | }).toList(), 70 | ), 71 | Spacer(), 72 | GestureDetector( 73 | onTap: () => copyCode(context, code), 74 | child: Icon( 75 | Icons.copy, 76 | color: Theme.of(context).colorScheme.onSurface, 77 | size: 16, 78 | ), 79 | ) 80 | ], 81 | ), 82 | ), 83 | Padding( 84 | padding: const EdgeInsets.all(8.0), 85 | child: Text(code, style: codeStyle), 86 | ), 87 | ], 88 | ), 89 | ), 90 | ); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /lib/views/widgets/cubic_curve_input_widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/services.dart'; 3 | 4 | class CubicCurveInputWidget extends StatelessWidget { 5 | final List controllers; 6 | final void Function(Curve curve)? onApply; 7 | 8 | const CubicCurveInputWidget({ 9 | super.key, 10 | required this.controllers, 11 | this.onApply, 12 | }); 13 | 14 | void _onPressed() { 15 | if (onApply == null) return; 16 | 17 | final x1 = double.parse(controllers[0].text); 18 | final y1 = double.parse(controllers[1].text); 19 | final x2 = double.parse(controllers[2].text); 20 | final y2 = double.parse(controllers[3].text); 21 | onApply?.call(Cubic(x1, y1, x2, y2)); 22 | } 23 | 24 | @override 25 | Widget build(BuildContext context) { 26 | final theme = Theme.of(context); 27 | 28 | final titleStyle = theme.textTheme.titleSmall; 29 | return LayoutBuilder(builder: (context, constraints) { 30 | return Container( 31 | padding: EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0), 32 | decoration: BoxDecoration( 33 | color: Theme.of(context).colorScheme.onPrimaryFixed, 34 | borderRadius: BorderRadius.circular(10), 35 | ), 36 | child: Column( 37 | spacing: 10, 38 | crossAxisAlignment: CrossAxisAlignment.start, 39 | mainAxisSize: MainAxisSize.min, 40 | children: [ 41 | Text("Control Points", style: titleStyle), 42 | Flex( 43 | direction: Axis.horizontal, 44 | children: [ 45 | _TextField( 46 | key: UniqueKey(), 47 | label: "X1", 48 | controller: controllers[0], 49 | ), 50 | _TextField( 51 | key: UniqueKey(), 52 | label: "Y1", 53 | controller: controllers[1], 54 | ), 55 | _TextField( 56 | key: UniqueKey(), 57 | label: "X2", 58 | controller: controllers[2], 59 | ), 60 | _TextField( 61 | key: UniqueKey(), 62 | label: "Y2", 63 | controller: controllers[3], 64 | ), 65 | ], 66 | ), 67 | const SizedBox(height: 10), 68 | MaterialButton( 69 | onPressed: _onPressed, 70 | mouseCursor: WidgetStateMouseCursor.clickable, 71 | color: theme.colorScheme.primary, 72 | padding: EdgeInsets.all(12.0), 73 | shape: RoundedRectangleBorder( 74 | borderRadius: BorderRadius.circular(5), 75 | ), 76 | minWidth: constraints.maxWidth, 77 | child: Text( 78 | "Apply", 79 | style: theme.primaryTextTheme.titleMedium!.copyWith( 80 | color: theme.colorScheme.onPrimary, 81 | ), 82 | ), 83 | ), 84 | const SizedBox(height: 0), 85 | ], 86 | ), 87 | ); 88 | }); 89 | } 90 | } 91 | 92 | class _TextField extends StatelessWidget { 93 | final String label; 94 | final TextEditingController? controller; 95 | 96 | const _TextField({ 97 | super.key, 98 | required this.label, 99 | this.controller, 100 | }); 101 | 102 | @override 103 | Widget build(BuildContext context) { 104 | return Flexible( 105 | child: TextField( 106 | controller: controller, 107 | keyboardType: TextInputType.numberWithOptions(decimal: true), 108 | inputFormatters: [ 109 | FilteringTextInputFormatter.allow(RegExp(r'^\d*\.?\d*')), 110 | ], 111 | decoration: InputDecoration( 112 | labelText: label, 113 | ), 114 | ), 115 | ); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /lib/views/widgets/cubic_graph.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class CubicGraph extends StatefulWidget { 4 | const CubicGraph({super.key}); 5 | 6 | @override 7 | State createState() => _CubicGraphState(); 8 | } 9 | 10 | class _CubicGraphState extends State { 11 | Offset x2Position = const Offset(100, 100); 12 | Offset y2Position = const Offset(200, 200); 13 | bool isDraggingX2 = false; 14 | bool isDraggingY2 = false; 15 | 16 | void _updatePosition(Offset position) { 17 | setState(() { 18 | if (isDraggingX2) { 19 | x2Position = position; 20 | } else if (isDraggingY2) { 21 | y2Position = position; 22 | } 23 | }); 24 | } 25 | 26 | @override 27 | Widget build(BuildContext context) { 28 | final screenSize = MediaQuery.sizeOf(context); 29 | 30 | // Size of the actual graph area 31 | final graphSize = Size(350, 300); 32 | 33 | // Size of the draggable area 34 | final containerSize = Size(screenSize.width, screenSize.height); 35 | 36 | // Offset to center the graph in the container 37 | final graphOffset = Offset( 38 | (containerSize.width - graphSize.width) / 2, 39 | (containerSize.height - graphSize.height) / 2, 40 | ); 41 | 42 | return MouseRegion( 43 | cursor: isDraggingX2 || isDraggingY2 44 | ? SystemMouseCursors.grabbing 45 | : SystemMouseCursors.basic, 46 | child: GestureDetector( 47 | onPanStart: (details) { 48 | final pos = details.localPosition; 49 | if ((pos - (x2Position + graphOffset)).distance < 20) { 50 | setState(() => isDraggingX2 = true); 51 | } else if ((pos - (y2Position + graphOffset)).distance < 20) { 52 | setState(() => isDraggingY2 = true); 53 | } 54 | }, 55 | onPanUpdate: (details) => 56 | _updatePosition(details.localPosition - graphOffset), 57 | onPanEnd: (_) => setState(() { 58 | isDraggingX2 = false; 59 | isDraggingY2 = false; 60 | }), 61 | onPanCancel: () => setState(() { 62 | isDraggingX2 = false; 63 | isDraggingY2 = false; 64 | }), 65 | child: Container( 66 | width: containerSize.width, 67 | height: containerSize.height, 68 | color: Colors.white, 69 | child: CustomPaint( 70 | size: containerSize, 71 | painter: CubicGraphPainter( 72 | x2Position: x2Position, 73 | y2Position: y2Position, 74 | isX2Active: isDraggingX2, 75 | isY2Active: isDraggingY2, 76 | graphSize: graphSize, 77 | graphOffset: graphOffset, 78 | ), 79 | ), 80 | ), 81 | ), 82 | ); 83 | } 84 | } 85 | 86 | class CubicGraphPainter extends CustomPainter { 87 | final Offset x2Position; 88 | final Offset y2Position; 89 | final bool isX2Active; 90 | final bool isY2Active; 91 | final Size graphSize; 92 | final Offset graphOffset; 93 | 94 | CubicGraphPainter({ 95 | required this.x2Position, 96 | required this.y2Position, 97 | required this.isX2Active, 98 | required this.isY2Active, 99 | required this.graphSize, 100 | required this.graphOffset, 101 | }); 102 | 103 | @override 104 | bool shouldRepaint(covariant CubicGraphPainter oldDelegate) { 105 | return x2Position != oldDelegate.x2Position || 106 | y2Position != oldDelegate.y2Position || 107 | isX2Active != oldDelegate.isX2Active || 108 | isY2Active != oldDelegate.isY2Active; 109 | } 110 | 111 | @override 112 | void paint(Canvas canvas, Size size) { 113 | canvas.translate(graphOffset.dx, graphOffset.dy); 114 | _drawAxisLines(canvas, graphSize); 115 | _drawBezierCurve(canvas, graphSize); 116 | _drawControlPoints(canvas, graphSize); 117 | } 118 | 119 | void _drawAxisLines(Canvas canvas, Size size) { 120 | final height = size.height; 121 | final width = size.width; 122 | 123 | final linePaint = Paint() 124 | ..color = Colors.grey 125 | ..strokeWidth = 1.75 126 | ..strokeCap = StrokeCap.round 127 | ..style = PaintingStyle.stroke; 128 | 129 | // Graph background 130 | canvas.drawRect( 131 | Offset.zero & size, 132 | Paint()..color = Colors.grey.withValues(alpha: 0.1), 133 | ); 134 | 135 | // x axis 136 | canvas.drawLine(Offset(0, height), Offset(width, height), linePaint); 137 | 138 | // y axis 139 | canvas.drawLine(Offset.zero, Offset(0, height), linePaint); 140 | } 141 | 142 | void _drawBezierCurve(Canvas canvas, Size size) { 143 | final curvePaint = Paint() 144 | ..color = Colors.purple 145 | ..strokeWidth = 2 146 | ..style = PaintingStyle.stroke; 147 | 148 | final path = Path(); 149 | path.moveTo(0, size.height); 150 | path.cubicTo( 151 | x2Position.dx, 152 | x2Position.dy, 153 | y2Position.dx, 154 | y2Position.dy, 155 | size.width, 156 | 0, 157 | ); 158 | 159 | canvas.drawPath(path, curvePaint); 160 | 161 | final controlLinePaint = Paint() 162 | ..color = Colors.grey 163 | ..strokeWidth = 1 164 | ..strokeCap = StrokeCap.round 165 | ..style = PaintingStyle.stroke; 166 | 167 | canvas.drawLine(Offset(0, size.height), x2Position, controlLinePaint); 168 | canvas.drawLine(y2Position, Offset(size.width, 0), controlLinePaint); 169 | } 170 | 171 | void _drawControlPoints(Canvas canvas, Size size) { 172 | final pointerPaint = Paint() 173 | ..strokeCap = StrokeCap.round 174 | ..style = PaintingStyle.fill; 175 | 176 | // Fixed points 177 | canvas.drawCircle( 178 | Offset(0, size.height), 179 | 10, 180 | pointerPaint..color = Colors.red, 181 | ); 182 | 183 | canvas.drawCircle( 184 | Offset(size.width, 0), 185 | 10, 186 | pointerPaint..color = Colors.red, 187 | ); 188 | 189 | // Draggable points 190 | canvas.drawCircle( 191 | x2Position, 192 | 10, 193 | pointerPaint..color = isX2Active ? Colors.blue : Colors.green, 194 | ); 195 | 196 | canvas.drawCircle( 197 | y2Position, 198 | 10, 199 | pointerPaint..color = isY2Active ? Colors.blue : Colors.green, 200 | ); 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /lib/views/widgets/dropdown_menu.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class DropdownMenuWidget extends StatelessWidget { 4 | final String title; 5 | final T? value; 6 | final List items; 7 | final Widget Function(BuildContext context, T value, TextStyle? textStyle)? 8 | builder; 9 | final void Function(T?)? onChanged; 10 | 11 | const DropdownMenuWidget({ 12 | super.key, 13 | required this.title, 14 | required this.items, 15 | this.builder, 16 | this.onChanged, 17 | this.value, 18 | }); 19 | 20 | @override 21 | Widget build(BuildContext context) { 22 | final titleStyle = Theme.of(context).textTheme.titleSmall; 23 | 24 | final childTextStyle = Theme.of(context).textTheme.titleMedium; 25 | 26 | return PhysicalModel( 27 | color: Theme.of(context).colorScheme.onPrimaryFixed, 28 | shadowColor: Theme.of(context).colorScheme.shadow, 29 | elevation: 1.0, 30 | borderRadius: BorderRadius.circular(10), 31 | child: Padding( 32 | padding: EdgeInsets.fromLTRB(16, 8, 16, 0.0), 33 | child: Column( 34 | crossAxisAlignment: CrossAxisAlignment.start, 35 | children: [ 36 | Text(title, style: titleStyle), 37 | DropdownButton( 38 | value: value, 39 | onChanged: onChanged, 40 | underline: const SizedBox.shrink(), 41 | isExpanded: true, 42 | isDense: false, 43 | items: items.map( 44 | (item) { 45 | return DropdownMenuItem( 46 | value: item, 47 | child: builder?.call(context, item, childTextStyle) ?? 48 | Text(item.toString(), style: childTextStyle), 49 | ); 50 | }, 51 | ).toList(), 52 | ), 53 | ], 54 | ), 55 | ), 56 | ); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /lib/views/widgets/graph/graph_config.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class GraphConfiguration { 4 | final int xAxisLineCount; 5 | final int yAxisLineCount; 6 | final int curveDivisions; 7 | final Color axisColor; 8 | final Color curveLineColor; 9 | final Color curveOutlineColor; 10 | final Color graphMarkerColor; 11 | final double axisWidth; 12 | final double curveLineWidth; 13 | final double pointerSize; 14 | final bool showCurveOutline; 15 | 16 | const GraphConfiguration({ 17 | this.xAxisLineCount = 10, 18 | this.yAxisLineCount = 10, 19 | this.curveDivisions = 1000, 20 | required this.axisColor, 21 | required this.curveLineColor, 22 | required this.graphMarkerColor, 23 | required this.axisWidth, 24 | required this.curveLineWidth, 25 | required this.pointerSize, 26 | required this.curveOutlineColor, 27 | this.showCurveOutline = false, 28 | }) : assert(pointerSize > 0, 'pointerSize must be greater than 0'), 29 | assert(axisWidth > 0, 'axisWidth must be greater than 0'), 30 | assert(curveLineWidth > 0, 'curveLineWidth must be greater than 0'), 31 | assert(curveDivisions > 0, 'curveDivisions must be greater than 0'), 32 | assert(xAxisLineCount >= 0, 'graphDivisions must be not negative'); 33 | } 34 | -------------------------------------------------------------------------------- /lib/views/widgets/graph/graph_painter.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'graph_config.dart'; 3 | 4 | class GraphPainter extends CustomPainter { 5 | const GraphPainter({ 6 | required this.controller, 7 | required this.animation, 8 | required this.config, 9 | }) : super(repaint: animation); 10 | 11 | final CurvedAnimation animation; 12 | final AnimationController controller; 13 | final GraphConfiguration config; 14 | 15 | int get divisions => config.curveDivisions; 16 | 17 | @override 18 | bool shouldRepaint(GraphPainter oldDelegate) => 19 | controller.value != oldDelegate.controller.value; 20 | 21 | @override 22 | void paint(Canvas canvas, Size size) { 23 | final points = generateCurveValues(animation, divisions); 24 | 25 | _drawAxis(canvas, size); 26 | _drawCurveBg(canvas, points, size); 27 | _drawCurve(canvas, points, size); 28 | _drawCurrentValueMarker(canvas, size, points); 29 | } 30 | 31 | void _drawCurrentValueMarker(Canvas canvas, Size size, List points) { 32 | final xPos = controller.value * size.width; 33 | final yPos = (1 - points[(controller.value * (divisions - 1)).floor()]) * 34 | size.height; 35 | 36 | Paint markerPaint = Paint() 37 | ..color = config.graphMarkerColor 38 | ..strokeCap = StrokeCap.round 39 | ..isAntiAlias = true; 40 | 41 | canvas.drawCircle(Offset(xPos, yPos), config.pointerSize, markerPaint); 42 | } 43 | 44 | void _drawCurve(Canvas canvas, List points, Size size) { 45 | Paint curvePaint = Paint() 46 | ..color = config.curveLineColor 47 | ..strokeCap = StrokeCap.round 48 | ..strokeWidth = config.curveLineWidth 49 | ..style = PaintingStyle.stroke; 50 | 51 | Path path = Path(); 52 | 53 | // Start the path at the first point 54 | path.moveTo(0, (1 - points.first) * size.height); 55 | 56 | // Draw line segments based on the generated curve points 57 | for (int i = 0; i < points.length; i++) { 58 | // current point of the curve 59 | final currentX = controller.value * size.width; 60 | final currentY = (1 - points[i]) * size.height; 61 | 62 | // point to draw 63 | final pointX = (i / divisions * size.width); 64 | final pointY = (1 - points[i]) * size.height; 65 | 66 | // skip points that are ahead the current point 67 | // draw only the points that are behind the current point 68 | if (pointX >= currentX && pointY >= currentY) { 69 | continue; 70 | } 71 | 72 | path.lineTo(pointX, pointY); 73 | } 74 | 75 | // Draw the curve on the canvas 76 | canvas.drawPath(path, curvePaint); 77 | } 78 | 79 | void _drawCurveBg(Canvas canvas, List points, Size size) { 80 | if (!config.showCurveOutline) return; 81 | 82 | Paint curvePaint = Paint() 83 | ..color = config.curveOutlineColor 84 | ..strokeCap = StrokeCap.round 85 | ..strokeWidth = config.curveLineWidth 86 | ..style = PaintingStyle.stroke; 87 | 88 | Path path = Path(); 89 | 90 | // Start the path at the first point 91 | path.moveTo(0, (1 - points.first) * size.height); 92 | 93 | // Draw line segments based on the generated curve points 94 | for (int i = 1; i < points.length; i++) { 95 | // point to draw 96 | final pointX = (i / divisions * size.width); 97 | final pointY = (1 - points[i]) * size.height; 98 | 99 | path.lineTo(pointX, pointY); 100 | } 101 | 102 | // Draw the curve on the canvas 103 | canvas.drawPath(path, curvePaint); 104 | } 105 | 106 | void _drawAxis(Canvas canvas, Size size) { 107 | Paint axisPaint = Paint() 108 | ..color = config.axisColor 109 | ..strokeWidth = config.axisWidth 110 | ..isAntiAlias = true 111 | ..strokeCap = StrokeCap.round 112 | ..style = PaintingStyle.stroke; 113 | 114 | // Draw X-axis lines 115 | for (int i = 0; i <= config.xAxisLineCount; i++) { 116 | final x = i / config.xAxisLineCount * size.width; 117 | final y = size.height; 118 | canvas.drawLine(Offset(x, 0), Offset(x, y), axisPaint); 119 | } 120 | 121 | for (int i = 0; i <= config.yAxisLineCount; i++) { 122 | final x = size.width; 123 | final y = i / config.yAxisLineCount * size.height; 124 | canvas.drawLine(Offset(0, y), Offset(x, y), axisPaint); 125 | } 126 | } 127 | } 128 | 129 | List generateCurveValues(CurvedAnimation anim, int divisions) { 130 | return List.generate( 131 | divisions, 132 | (index) { 133 | double progress = index / (divisions - 1); 134 | return anim.curve.transform(progress); 135 | }, 136 | ); 137 | } 138 | -------------------------------------------------------------------------------- /lib/views/widgets/graph/graph_widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_curve_visualizer/views/widgets/graph/graph_config.dart'; 3 | import 'package:flutter_curve_visualizer/views/widgets/screen_mode.dart'; 4 | import 'graph_painter.dart'; 5 | 6 | class GraphWidget extends StatelessWidget { 7 | final bool showCurveOutline; 8 | final CurvedAnimation animation; 9 | final AnimationController controller; 10 | 11 | const GraphWidget({ 12 | super.key, 13 | this.showCurveOutline = true, 14 | required this.animation, 15 | required this.controller, 16 | }); 17 | 18 | @override 19 | Widget build(BuildContext context) { 20 | final theme = Theme.of(context).colorScheme; 21 | 22 | final mode = ScreenModeWidget.of(context); 23 | 24 | final widthToTake = switch (mode) { 25 | ScreenMode.mobile => 0.8, 26 | ScreenMode.tablet => 0.75, 27 | ScreenMode.web => 0.35, 28 | }; 29 | 30 | final screenSize = MediaQuery.sizeOf(context); 31 | 32 | final size = Size(350, screenSize.height / 4); 33 | 34 | return SizedBox( 35 | width: screenSize.width * widthToTake, 36 | height: screenSize.height / 3, 37 | child: Center( 38 | child: CustomPaint( 39 | size: size, 40 | painter: GraphPainter( 41 | controller: controller, 42 | animation: animation, 43 | config: GraphConfiguration( 44 | xAxisLineCount: 10, 45 | yAxisLineCount: 6, 46 | axisWidth: 1, 47 | curveLineWidth: 2.5, 48 | pointerSize: 5, 49 | axisColor: theme.onSurface.withValues(alpha: 0.25), 50 | showCurveOutline: showCurveOutline, 51 | curveOutlineColor: theme.onSurfaceVariant.withValues(alpha: 0.25), 52 | curveLineColor: theme.primaryFixed, 53 | graphMarkerColor: theme.primaryFixed, 54 | ), 55 | ), 56 | ), 57 | ), 58 | ); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /lib/views/widgets/screen_mode.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | enum ScreenMode { 4 | mobile, 5 | tablet, 6 | web; 7 | 8 | bool get isMobile => this == ScreenMode.mobile; 9 | 10 | bool get isTablet => this == ScreenMode.tablet; 11 | 12 | bool get isWeb => this == ScreenMode.web; 13 | 14 | bool get isMobileOrTablet => 15 | this == ScreenMode.mobile || this == ScreenMode.tablet; 16 | 17 | double get spacing => switch (this) { 18 | ScreenMode.mobile => 10, 19 | ScreenMode.tablet => 20, 20 | ScreenMode.web => 20, 21 | }; 22 | } 23 | 24 | class ScreenModeWidget extends InheritedWidget { 25 | final ScreenMode mode; 26 | 27 | const ScreenModeWidget({super.key, required super.child, required this.mode}); 28 | 29 | static ScreenMode of(BuildContext context) { 30 | final responsiveLayout = 31 | context.dependOnInheritedWidgetOfExactType(); 32 | 33 | if (responsiveLayout == null) { 34 | throw FlutterError('ResponsiveLayout not found in widget tree!'); 35 | } 36 | return responsiveLayout.mode; 37 | } 38 | 39 | @override 40 | bool updateShouldNotify(covariant ScreenModeWidget oldWidget) => 41 | mode != oldWidget.mode; 42 | } 43 | -------------------------------------------------------------------------------- /lib/views/widgets/time_slider.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class TimeSlider extends StatelessWidget { 4 | const TimeSlider({super.key, required this.animationTime, this.onChanged}); 5 | 6 | final int animationTime; 7 | final void Function(double)? onChanged; 8 | 9 | @override 10 | Widget build(BuildContext context) { 11 | final borderRadius = BorderRadius.circular(10); 12 | 13 | return PhysicalModel( 14 | color: Colors.transparent, 15 | shadowColor: Theme.of(context).colorScheme.shadow, 16 | elevation: 1.0, 17 | borderRadius: borderRadius, 18 | child: Container( 19 | padding: EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0), 20 | decoration: BoxDecoration( 21 | color: Theme.of(context).colorScheme.onPrimaryFixed, 22 | borderRadius: borderRadius, 23 | ), 24 | child: Column( 25 | crossAxisAlignment: CrossAxisAlignment.start, 26 | children: [ 27 | Text( 28 | "Time: ${animationTime}s", 29 | style: Theme.of(context).textTheme.titleSmall, 30 | ), 31 | Slider( 32 | year2023: false, 33 | value: animationTime.toDouble(), 34 | min: 1.0, 35 | max: 10.0, 36 | inactiveColor: Theme.of(context).colorScheme.surfaceDim, 37 | onChanged: onChanged, 38 | ), 39 | ], 40 | ), 41 | ), 42 | ); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /makefile: -------------------------------------------------------------------------------- 1 | BASE_HREF = '/' 2 | GITHUB_REPO := https://github.com/vchib1/vchib1.github.io.git 3 | 4 | clean: 5 | @echo "Cleaning up..." 6 | flutter clean 7 | @echo "Getting Packages..." 8 | flutter pub get 9 | @echo "Success..." 10 | 11 | build: 12 | @echo "Cleaning up..." 13 | flutter clean 14 | @echo "Getting Packages..." 15 | flutter pub get 16 | @echo "Building Web..." 17 | flutter build web --wasm 18 | 19 | deploy: 20 | make clean 21 | @echo "Building Web..." 22 | flutter build web --base-href $(BASE_HREF) --release 23 | @echo "Deploying Web to Repository..." 24 | cd build/web && \ 25 | git init && \ 26 | git add . && \ 27 | git commit -m "$(msg)" && \ 28 | git branch -m main && \ 29 | git remote add origin $(GITHUB_REPO) && \ 30 | git push -f origin main 31 | 32 | cd ../.. 33 | @echo "Deployment Success :)" 34 | 35 | host: 36 | make clean 37 | flutter run -d web-server --web-port 8080 --web-hostname 0.0.0.0 --profile 38 | @echo "Web server running on http://localhost:8080" 39 | 40 | 41 | .phony: host clean deploy -------------------------------------------------------------------------------- /pubspec.lock: -------------------------------------------------------------------------------- 1 | # Generated by pub 2 | # See https://dart.dev/tools/pub/glossary#lockfile 3 | packages: 4 | args: 5 | dependency: transitive 6 | description: 7 | name: args 8 | sha256: bf9f5caeea8d8fe6721a9c358dd8a5c1947b27f1cfaa18b39c301273594919e6 9 | url: "https://pub.dev" 10 | source: hosted 11 | version: "2.6.0" 12 | async: 13 | dependency: transitive 14 | description: 15 | name: async 16 | sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63 17 | url: "https://pub.dev" 18 | source: hosted 19 | version: "2.12.0" 20 | boolean_selector: 21 | dependency: transitive 22 | description: 23 | name: boolean_selector 24 | sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" 25 | url: "https://pub.dev" 26 | source: hosted 27 | version: "2.1.2" 28 | characters: 29 | dependency: transitive 30 | description: 31 | name: characters 32 | sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 33 | url: "https://pub.dev" 34 | source: hosted 35 | version: "1.4.0" 36 | clock: 37 | dependency: transitive 38 | description: 39 | name: clock 40 | sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b 41 | url: "https://pub.dev" 42 | source: hosted 43 | version: "1.1.2" 44 | collection: 45 | dependency: transitive 46 | description: 47 | name: collection 48 | sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" 49 | url: "https://pub.dev" 50 | source: hosted 51 | version: "1.19.1" 52 | cupertino_icons: 53 | dependency: "direct main" 54 | description: 55 | name: cupertino_icons 56 | sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 57 | url: "https://pub.dev" 58 | source: hosted 59 | version: "1.0.8" 60 | equatable: 61 | dependency: "direct main" 62 | description: 63 | name: equatable 64 | sha256: "567c64b3cb4cf82397aac55f4f0cbd3ca20d77c6c03bedbc4ceaddc08904aef7" 65 | url: "https://pub.dev" 66 | source: hosted 67 | version: "2.0.7" 68 | fake_async: 69 | dependency: transitive 70 | description: 71 | name: fake_async 72 | sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc" 73 | url: "https://pub.dev" 74 | source: hosted 75 | version: "1.3.2" 76 | ffi: 77 | dependency: transitive 78 | description: 79 | name: ffi 80 | sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418" 81 | url: "https://pub.dev" 82 | source: hosted 83 | version: "2.1.4" 84 | file: 85 | dependency: transitive 86 | description: 87 | name: file 88 | sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 89 | url: "https://pub.dev" 90 | source: hosted 91 | version: "7.0.1" 92 | flutter: 93 | dependency: "direct main" 94 | description: flutter 95 | source: sdk 96 | version: "0.0.0" 97 | flutter_animate: 98 | dependency: "direct main" 99 | description: 100 | name: flutter_animate 101 | sha256: "7befe2d3252728afb77aecaaea1dec88a89d35b9b1d2eea6d04479e8af9117b5" 102 | url: "https://pub.dev" 103 | source: hosted 104 | version: "4.5.2" 105 | flutter_lints: 106 | dependency: "direct dev" 107 | description: 108 | name: flutter_lints 109 | sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1" 110 | url: "https://pub.dev" 111 | source: hosted 112 | version: "5.0.0" 113 | flutter_shaders: 114 | dependency: transitive 115 | description: 116 | name: flutter_shaders 117 | sha256: "34794acadd8275d971e02df03afee3dee0f98dbfb8c4837082ad0034f612a3e2" 118 | url: "https://pub.dev" 119 | source: hosted 120 | version: "0.1.3" 121 | flutter_svg: 122 | dependency: "direct main" 123 | description: 124 | name: flutter_svg 125 | sha256: c200fd79c918a40c5cd50ea0877fa13f81bdaf6f0a5d3dbcc2a13e3285d6aa1b 126 | url: "https://pub.dev" 127 | source: hosted 128 | version: "2.0.17" 129 | flutter_test: 130 | dependency: "direct dev" 131 | description: flutter 132 | source: sdk 133 | version: "0.0.0" 134 | flutter_web_plugins: 135 | dependency: transitive 136 | description: flutter 137 | source: sdk 138 | version: "0.0.0" 139 | http: 140 | dependency: transitive 141 | description: 142 | name: http 143 | sha256: fe7ab022b76f3034adc518fb6ea04a82387620e19977665ea18d30a1cf43442f 144 | url: "https://pub.dev" 145 | source: hosted 146 | version: "1.3.0" 147 | http_parser: 148 | dependency: transitive 149 | description: 150 | name: http_parser 151 | sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" 152 | url: "https://pub.dev" 153 | source: hosted 154 | version: "4.1.2" 155 | leak_tracker: 156 | dependency: transitive 157 | description: 158 | name: leak_tracker 159 | sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec 160 | url: "https://pub.dev" 161 | source: hosted 162 | version: "10.0.8" 163 | leak_tracker_flutter_testing: 164 | dependency: transitive 165 | description: 166 | name: leak_tracker_flutter_testing 167 | sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573 168 | url: "https://pub.dev" 169 | source: hosted 170 | version: "3.0.9" 171 | leak_tracker_testing: 172 | dependency: transitive 173 | description: 174 | name: leak_tracker_testing 175 | sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" 176 | url: "https://pub.dev" 177 | source: hosted 178 | version: "3.0.1" 179 | light_dark_theme_toggle: 180 | dependency: "direct main" 181 | description: 182 | name: light_dark_theme_toggle 183 | sha256: "2e4b9fc7d16407f542b809195c9c69c74513d54b19cc225bcde2f484c27cdefe" 184 | url: "https://pub.dev" 185 | source: hosted 186 | version: "1.1.2" 187 | lints: 188 | dependency: transitive 189 | description: 190 | name: lints 191 | sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7 192 | url: "https://pub.dev" 193 | source: hosted 194 | version: "5.1.1" 195 | matcher: 196 | dependency: transitive 197 | description: 198 | name: matcher 199 | sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 200 | url: "https://pub.dev" 201 | source: hosted 202 | version: "0.12.17" 203 | material_color_utilities: 204 | dependency: transitive 205 | description: 206 | name: material_color_utilities 207 | sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec 208 | url: "https://pub.dev" 209 | source: hosted 210 | version: "0.11.1" 211 | meta: 212 | dependency: transitive 213 | description: 214 | name: meta 215 | sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c 216 | url: "https://pub.dev" 217 | source: hosted 218 | version: "1.16.0" 219 | nested: 220 | dependency: transitive 221 | description: 222 | name: nested 223 | sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" 224 | url: "https://pub.dev" 225 | source: hosted 226 | version: "1.0.0" 227 | path: 228 | dependency: transitive 229 | description: 230 | name: path 231 | sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" 232 | url: "https://pub.dev" 233 | source: hosted 234 | version: "1.9.1" 235 | path_parsing: 236 | dependency: transitive 237 | description: 238 | name: path_parsing 239 | sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca" 240 | url: "https://pub.dev" 241 | source: hosted 242 | version: "1.1.0" 243 | path_provider_linux: 244 | dependency: transitive 245 | description: 246 | name: path_provider_linux 247 | sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 248 | url: "https://pub.dev" 249 | source: hosted 250 | version: "2.2.1" 251 | path_provider_platform_interface: 252 | dependency: transitive 253 | description: 254 | name: path_provider_platform_interface 255 | sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" 256 | url: "https://pub.dev" 257 | source: hosted 258 | version: "2.1.2" 259 | path_provider_windows: 260 | dependency: transitive 261 | description: 262 | name: path_provider_windows 263 | sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 264 | url: "https://pub.dev" 265 | source: hosted 266 | version: "2.3.0" 267 | petitparser: 268 | dependency: transitive 269 | description: 270 | name: petitparser 271 | sha256: "07c8f0b1913bcde1ff0d26e57ace2f3012ccbf2b204e070290dad3bb22797646" 272 | url: "https://pub.dev" 273 | source: hosted 274 | version: "6.1.0" 275 | platform: 276 | dependency: transitive 277 | description: 278 | name: platform 279 | sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" 280 | url: "https://pub.dev" 281 | source: hosted 282 | version: "3.1.6" 283 | plugin_platform_interface: 284 | dependency: transitive 285 | description: 286 | name: plugin_platform_interface 287 | sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" 288 | url: "https://pub.dev" 289 | source: hosted 290 | version: "2.1.8" 291 | provider: 292 | dependency: "direct main" 293 | description: 294 | name: provider 295 | sha256: c8a055ee5ce3fd98d6fc872478b03823ffdb448699c6ebdbbc71d59b596fd48c 296 | url: "https://pub.dev" 297 | source: hosted 298 | version: "6.1.2" 299 | shared_preferences: 300 | dependency: "direct main" 301 | description: 302 | name: shared_preferences 303 | sha256: "846849e3e9b68f3ef4b60c60cf4b3e02e9321bc7f4d8c4692cf87ffa82fc8a3a" 304 | url: "https://pub.dev" 305 | source: hosted 306 | version: "2.5.2" 307 | shared_preferences_android: 308 | dependency: transitive 309 | description: 310 | name: shared_preferences_android 311 | sha256: a768fc8ede5f0c8e6150476e14f38e2417c0864ca36bb4582be8e21925a03c22 312 | url: "https://pub.dev" 313 | source: hosted 314 | version: "2.4.6" 315 | shared_preferences_foundation: 316 | dependency: transitive 317 | description: 318 | name: shared_preferences_foundation 319 | sha256: "6a52cfcdaeac77cad8c97b539ff688ccfc458c007b4db12be584fbe5c0e49e03" 320 | url: "https://pub.dev" 321 | source: hosted 322 | version: "2.5.4" 323 | shared_preferences_linux: 324 | dependency: transitive 325 | description: 326 | name: shared_preferences_linux 327 | sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" 328 | url: "https://pub.dev" 329 | source: hosted 330 | version: "2.4.1" 331 | shared_preferences_platform_interface: 332 | dependency: transitive 333 | description: 334 | name: shared_preferences_platform_interface 335 | sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" 336 | url: "https://pub.dev" 337 | source: hosted 338 | version: "2.4.1" 339 | shared_preferences_web: 340 | dependency: transitive 341 | description: 342 | name: shared_preferences_web 343 | sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019 344 | url: "https://pub.dev" 345 | source: hosted 346 | version: "2.4.3" 347 | shared_preferences_windows: 348 | dependency: transitive 349 | description: 350 | name: shared_preferences_windows 351 | sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" 352 | url: "https://pub.dev" 353 | source: hosted 354 | version: "2.4.1" 355 | sky_engine: 356 | dependency: transitive 357 | description: flutter 358 | source: sdk 359 | version: "0.0.0" 360 | source_span: 361 | dependency: transitive 362 | description: 363 | name: source_span 364 | sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" 365 | url: "https://pub.dev" 366 | source: hosted 367 | version: "1.10.1" 368 | stack_trace: 369 | dependency: transitive 370 | description: 371 | name: stack_trace 372 | sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" 373 | url: "https://pub.dev" 374 | source: hosted 375 | version: "1.12.1" 376 | stream_channel: 377 | dependency: transitive 378 | description: 379 | name: stream_channel 380 | sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" 381 | url: "https://pub.dev" 382 | source: hosted 383 | version: "2.1.4" 384 | string_scanner: 385 | dependency: transitive 386 | description: 387 | name: string_scanner 388 | sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" 389 | url: "https://pub.dev" 390 | source: hosted 391 | version: "1.4.1" 392 | term_glyph: 393 | dependency: transitive 394 | description: 395 | name: term_glyph 396 | sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" 397 | url: "https://pub.dev" 398 | source: hosted 399 | version: "1.2.2" 400 | test_api: 401 | dependency: transitive 402 | description: 403 | name: test_api 404 | sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd 405 | url: "https://pub.dev" 406 | source: hosted 407 | version: "0.7.4" 408 | typed_data: 409 | dependency: transitive 410 | description: 411 | name: typed_data 412 | sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 413 | url: "https://pub.dev" 414 | source: hosted 415 | version: "1.4.0" 416 | url_launcher: 417 | dependency: "direct main" 418 | description: 419 | name: url_launcher 420 | sha256: "9d06212b1362abc2f0f0d78e6f09f726608c74e3b9462e8368bb03314aa8d603" 421 | url: "https://pub.dev" 422 | source: hosted 423 | version: "6.3.1" 424 | url_launcher_android: 425 | dependency: transitive 426 | description: 427 | name: url_launcher_android 428 | sha256: "6fc2f56536ee873eeb867ad176ae15f304ccccc357848b351f6f0d8d4a40d193" 429 | url: "https://pub.dev" 430 | source: hosted 431 | version: "6.3.14" 432 | url_launcher_ios: 433 | dependency: transitive 434 | description: 435 | name: url_launcher_ios 436 | sha256: "16a513b6c12bb419304e72ea0ae2ab4fed569920d1c7cb850263fe3acc824626" 437 | url: "https://pub.dev" 438 | source: hosted 439 | version: "6.3.2" 440 | url_launcher_linux: 441 | dependency: transitive 442 | description: 443 | name: url_launcher_linux 444 | sha256: "4e9ba368772369e3e08f231d2301b4ef72b9ff87c31192ef471b380ef29a4935" 445 | url: "https://pub.dev" 446 | source: hosted 447 | version: "3.2.1" 448 | url_launcher_macos: 449 | dependency: transitive 450 | description: 451 | name: url_launcher_macos 452 | sha256: "17ba2000b847f334f16626a574c702b196723af2a289e7a93ffcb79acff855c2" 453 | url: "https://pub.dev" 454 | source: hosted 455 | version: "3.2.2" 456 | url_launcher_platform_interface: 457 | dependency: transitive 458 | description: 459 | name: url_launcher_platform_interface 460 | sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" 461 | url: "https://pub.dev" 462 | source: hosted 463 | version: "2.3.2" 464 | url_launcher_web: 465 | dependency: transitive 466 | description: 467 | name: url_launcher_web 468 | sha256: "3ba963161bd0fe395917ba881d320b9c4f6dd3c4a233da62ab18a5025c85f1e9" 469 | url: "https://pub.dev" 470 | source: hosted 471 | version: "2.4.0" 472 | url_launcher_windows: 473 | dependency: transitive 474 | description: 475 | name: url_launcher_windows 476 | sha256: "3284b6d2ac454cf34f114e1d3319866fdd1e19cdc329999057e44ffe936cfa77" 477 | url: "https://pub.dev" 478 | source: hosted 479 | version: "3.1.4" 480 | vector_graphics: 481 | dependency: transitive 482 | description: 483 | name: vector_graphics 484 | sha256: "44cc7104ff32563122a929e4620cf3efd584194eec6d1d913eb5ba593dbcf6de" 485 | url: "https://pub.dev" 486 | source: hosted 487 | version: "1.1.18" 488 | vector_graphics_codec: 489 | dependency: transitive 490 | description: 491 | name: vector_graphics_codec 492 | sha256: "99fd9fbd34d9f9a32efd7b6a6aae14125d8237b10403b422a6a6dfeac2806146" 493 | url: "https://pub.dev" 494 | source: hosted 495 | version: "1.1.13" 496 | vector_graphics_compiler: 497 | dependency: transitive 498 | description: 499 | name: vector_graphics_compiler 500 | sha256: "1b4b9e706a10294258727674a340ae0d6e64a7231980f9f9a3d12e4b42407aad" 501 | url: "https://pub.dev" 502 | source: hosted 503 | version: "1.1.16" 504 | vector_math: 505 | dependency: transitive 506 | description: 507 | name: vector_math 508 | sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" 509 | url: "https://pub.dev" 510 | source: hosted 511 | version: "2.1.4" 512 | vm_service: 513 | dependency: transitive 514 | description: 515 | name: vm_service 516 | sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14" 517 | url: "https://pub.dev" 518 | source: hosted 519 | version: "14.3.1" 520 | web: 521 | dependency: transitive 522 | description: 523 | name: web 524 | sha256: cd3543bd5798f6ad290ea73d210f423502e71900302dde696f8bff84bf89a1cb 525 | url: "https://pub.dev" 526 | source: hosted 527 | version: "1.1.0" 528 | xdg_directories: 529 | dependency: transitive 530 | description: 531 | name: xdg_directories 532 | sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" 533 | url: "https://pub.dev" 534 | source: hosted 535 | version: "1.1.0" 536 | xml: 537 | dependency: transitive 538 | description: 539 | name: xml 540 | sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226 541 | url: "https://pub.dev" 542 | source: hosted 543 | version: "6.5.0" 544 | sdks: 545 | dart: ">=3.7.0 <4.0.0" 546 | flutter: ">=3.27.0" 547 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: flutter_curve_visualizer 2 | description: "A new Flutter project." 3 | # The following line prevents the package from being accidentally published to 4 | # pub.dev using `flutter pub publish`. This is preferred for private packages. 5 | publish_to: 'none' # Remove this line if you wish to publish to pub.dev 6 | 7 | # The following defines the version and build number for your application. 8 | # A version number is three numbers separated by dots, like 1.2.43 9 | # followed by an optional build number separated by a +. 10 | # Both the version and the builder number may be overridden in flutter 11 | # build by specifying --build-name and --build-number, respectively. 12 | # In Android, build-name is used as versionName while build-number used as versionCode. 13 | # Read more about Android versioning at https://developer.android.com/studio/publish/versioning 14 | # In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion. 15 | # Read more about iOS versioning at 16 | # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html 17 | # In Windows, build-name is used as the major, minor, and patch parts 18 | # of the product and file versions while build-number is used as the build suffix. 19 | version: 1.0.0+1 20 | 21 | environment: 22 | sdk: ^3.7.0 23 | 24 | # Dependencies specify other packages that your package needs in order to work. 25 | # To automatically upgrade your package dependencies to the latest versions 26 | # consider running `flutter pub upgrade --major-versions`. Alternatively, 27 | # dependencies can be manually updated by changing the version numbers below to 28 | # the latest version available on pub.dev. To see which dependencies have newer 29 | # versions available, run `flutter pub outdated`. 30 | dependencies: 31 | flutter: 32 | sdk: flutter 33 | 34 | # The following adds the Cupertino Icons font to your application. 35 | # Use with the CupertinoIcons class for iOS style icons. 36 | cupertino_icons: ^1.0.8 37 | shared_preferences: ^2.3.4 38 | provider: ^6.1.2 39 | url_launcher: ^6.3.1 40 | flutter_svg: ^2.0.16 41 | flutter_animate: ^4.5.2 42 | equatable: ^2.0.7 43 | light_dark_theme_toggle: ^1.0.0 44 | 45 | dev_dependencies: 46 | flutter_test: 47 | sdk: flutter 48 | 49 | # The "flutter_lints" package below contains a set of recommended lints to 50 | # encourage good coding practices. The lint set provided by the package is 51 | # activated in the `analysis_options.yaml` file located at the root of your 52 | # package. See that file for information about deactivating specific lint 53 | # rules and activating additional ones. 54 | flutter_lints: ^5.0.0 55 | 56 | # For information on the generic Dart part of this file, see the 57 | # following page: https://dart.dev/tools/pub/pubspec 58 | 59 | # The following section is specific to Flutter packages. 60 | flutter: 61 | 62 | # The following line ensures that the Material Icons font is 63 | # included with your application, so that you can use the icons in 64 | # the material Icons class. 65 | uses-material-design: true 66 | 67 | # To add assets to your application, add an assets section, like this: 68 | assets: 69 | - assets/svg/ 70 | 71 | # An image asset can refer to one or more resolution-specific "variants", see 72 | # https://flutter.dev/to/resolution-aware-images 73 | 74 | # For details regarding adding assets from package dependencies, see 75 | # https://flutter.dev/to/asset-from-package 76 | 77 | # To add custom fonts to your application, add a fonts section here, 78 | # in this "flutter" section. Each entry in this list should have a 79 | # "family" key with the font family name, and a "fonts" key with a 80 | # list giving the asset and other descriptors for the font. For 81 | # example: 82 | fonts: 83 | - family: Outfit 84 | fonts: 85 | - asset: fonts/Outfit-Bold.ttf 86 | - asset: fonts/Outfit-Regular.ttf 87 | - asset: fonts/Outfit-Medium.ttf 88 | - asset: fonts/Outfit-SemiBold.ttf 89 | - asset: fonts/Outfit-ExtraBold.ttf 90 | - asset: fonts/Outfit-ExtraLight.ttf 91 | - asset: fonts/Outfit-Light.ttf 92 | - asset: fonts/Outfit-Thin.ttf 93 | # - asset: fonts/Schyler-Italic.ttf 94 | # style: italic 95 | # - family: Trajan Pro 96 | # fonts: 97 | # - asset: fonts/TrajanPro.ttf 98 | # - asset: fonts/TrajanPro_Bold.ttf 99 | # weight: 700 100 | # 101 | # For details regarding fonts from package dependencies, 102 | # see https://flutter.dev/to/font-from-package 103 | -------------------------------------------------------------------------------- /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 in the flutter_test package. 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 'package:flutter_curve_visualizer/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(const 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 | -------------------------------------------------------------------------------- /web/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vchib1/flutter-curve-visualizer/fe547667e37b6e0c8283ece35958de785dd26455/web/favicon.png -------------------------------------------------------------------------------- /web/icons/Icon-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vchib1/flutter-curve-visualizer/fe547667e37b6e0c8283ece35958de785dd26455/web/icons/Icon-192.png -------------------------------------------------------------------------------- /web/icons/Icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vchib1/flutter-curve-visualizer/fe547667e37b6e0c8283ece35958de785dd26455/web/icons/Icon-512.png -------------------------------------------------------------------------------- /web/icons/Icon-maskable-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vchib1/flutter-curve-visualizer/fe547667e37b6e0c8283ece35958de785dd26455/web/icons/Icon-maskable-192.png -------------------------------------------------------------------------------- /web/icons/Icon-maskable-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vchib1/flutter-curve-visualizer/fe547667e37b6e0c8283ece35958de785dd26455/web/icons/Icon-maskable-512.png -------------------------------------------------------------------------------- /web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | Flutter Curve Visualizer 20 | 21 | 72 | 73 | 74 |
75 |
76 |
77 | 110 | 111 | 112 | -------------------------------------------------------------------------------- /web/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "flutter_curve_visualizer", 3 | "short_name": "flutter_curve_visualizer", 4 | "start_url": ".", 5 | "display": "standalone", 6 | "background_color": "#0175C2", 7 | "theme_color": "#0175C2", 8 | "description": "A new Flutter project.", 9 | "orientation": "portrait-primary", 10 | "prefer_related_applications": false, 11 | "icons": [ 12 | { 13 | "src": "icons/Icon-192.png", 14 | "sizes": "192x192", 15 | "type": "image/png" 16 | }, 17 | { 18 | "src": "icons/Icon-512.png", 19 | "sizes": "512x512", 20 | "type": "image/png" 21 | }, 22 | { 23 | "src": "icons/Icon-maskable-192.png", 24 | "sizes": "192x192", 25 | "type": "image/png", 26 | "purpose": "maskable" 27 | }, 28 | { 29 | "src": "icons/Icon-maskable-512.png", 30 | "sizes": "512x512", 31 | "type": "image/png", 32 | "purpose": "maskable" 33 | } 34 | ] 35 | } 36 | --------------------------------------------------------------------------------