| // Copyright 2014 The Flutter Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import 'package:flutter/foundation.dart'; |
| import 'package:flutter/material.dart'; |
| |
| void main() => runApp(const MyApp()); |
| |
| enum LerpTarget { |
| circle, |
| roundedRect, |
| rect, |
| stadium, |
| polygon, |
| star, |
| } |
| |
| class MyApp extends StatelessWidget { |
| const MyApp({super.key}); |
| |
| @override |
| Widget build(BuildContext context) { |
| return const MaterialApp( |
| home: MyHomePage(), |
| ); |
| } |
| } |
| |
| class MyHomePage extends StatefulWidget { |
| const MyHomePage({super.key}); |
| |
| @override |
| State<MyHomePage> createState() => _MyHomePageState(); |
| } |
| |
| class _MyHomePageState extends State<MyHomePage> { |
| static final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>(); |
| final OptionModel _model = OptionModel(); |
| final TextEditingController textController = TextEditingController(); |
| |
| @override |
| void initState() { |
| super.initState(); |
| _model.addListener(_modelChanged); |
| } |
| |
| @override |
| void dispose() { |
| super.dispose(); |
| _model.removeListener(_modelChanged); |
| } |
| |
| void _modelChanged() { |
| setState(() {}); |
| } |
| |
| @override |
| Widget build(BuildContext context) { |
| return SafeArea( |
| child: Scaffold( |
| key: scaffoldKey, |
| appBar: AppBar( |
| title: const Text('Star Border'), |
| backgroundColor: const Color(0xff323232), |
| ), |
| body: Column( |
| children: <Widget>[ |
| Container(color: Colors.grey.shade200, child: Options(_model)), |
| Expanded( |
| child: Row( |
| mainAxisAlignment: MainAxisAlignment.spaceEvenly, |
| children: <Widget>[ |
| Container( |
| key: UniqueKey(), |
| alignment: Alignment.center, |
| width: 300, |
| height: 200, |
| decoration: ShapeDecoration( |
| color: Colors.blue.shade100, |
| shape: lerpBorder( |
| StarBorder.polygon( |
| side: const BorderSide(strokeAlign: BorderSide.strokeAlignCenter, width: 2), |
| sides: _model.points, |
| pointRounding: _model.pointRounding, |
| rotation: _model.rotation, |
| squash: _model.squash, |
| ), |
| _model._lerpTarget, |
| _model._lerpAmount, |
| to: _model.lerpTo, |
| )!, |
| ), |
| child: const Text('Polygon'), |
| ), |
| Container( |
| key: UniqueKey(), |
| alignment: Alignment.center, |
| width: 300, |
| height: 200, |
| decoration: ShapeDecoration( |
| color: Colors.blue.shade100, |
| shape: lerpBorder( |
| StarBorder( |
| side: const BorderSide(strokeAlign: BorderSide.strokeAlignCenter, width: 2), |
| points: _model.points, |
| innerRadiusRatio: _model.innerRadiusRatio, |
| pointRounding: _model.pointRounding, |
| valleyRounding: _model.valleyRounding, |
| rotation: _model.rotation, |
| squash: _model.squash, |
| ), |
| _model._lerpTarget, |
| _model._lerpAmount, |
| to: _model.lerpTo, |
| )!, |
| ), |
| child: const Text('Star'), |
| ), |
| ], |
| ), |
| ), |
| ], |
| ), |
| ), |
| ); |
| } |
| } |
| |
| class OptionModel extends ChangeNotifier { |
| double get pointRounding => _pointRounding; |
| double _pointRounding = 0.0; |
| set pointRounding(double value) { |
| if (value != _pointRounding) { |
| _pointRounding = value; |
| if (_valleyRounding + _pointRounding > 1) { |
| _valleyRounding = 1.0 - _pointRounding; |
| } |
| notifyListeners(); |
| } |
| } |
| |
| double get valleyRounding => _valleyRounding; |
| double _valleyRounding = 0.0; |
| set valleyRounding(double value) { |
| if (value != _valleyRounding) { |
| _valleyRounding = value; |
| if (_valleyRounding + _pointRounding > 1) { |
| _pointRounding = 1.0 - _valleyRounding; |
| } |
| notifyListeners(); |
| } |
| } |
| |
| double get squash => _squash; |
| double _squash = 0.0; |
| set squash(double value) { |
| if (value != _squash) { |
| _squash = value; |
| notifyListeners(); |
| } |
| } |
| |
| double get rotation => _rotation; |
| double _rotation = 0.0; |
| set rotation(double value) { |
| if (value != _rotation) { |
| _rotation = value; |
| notifyListeners(); |
| } |
| } |
| |
| double get innerRadiusRatio => _innerRadiusRatio; |
| double _innerRadiusRatio = 0.4; |
| set innerRadiusRatio(double value) { |
| if (value != _innerRadiusRatio) { |
| _innerRadiusRatio = clampDouble(value, 0.0001, double.infinity); |
| notifyListeners(); |
| } |
| } |
| |
| double get points => _points; |
| double _points = 5; |
| set points(double value) { |
| if (value != _points) { |
| _points = value; |
| notifyListeners(); |
| } |
| } |
| |
| double get lerpAmount => _lerpAmount; |
| double _lerpAmount = 0.0; |
| set lerpAmount(double value) { |
| if (value != _lerpAmount) { |
| _lerpAmount = value; |
| notifyListeners(); |
| } |
| } |
| |
| bool get lerpTo => _lerpTo; |
| bool _lerpTo = true; |
| set lerpTo(bool value) { |
| if (_lerpTo != value) { |
| _lerpTo = value; |
| notifyListeners(); |
| } |
| } |
| |
| LerpTarget get lerpTarget => _lerpTarget; |
| LerpTarget _lerpTarget = LerpTarget.circle; |
| set lerpTarget(LerpTarget value) { |
| if (value != _lerpTarget) { |
| _lerpTarget = value; |
| notifyListeners(); |
| } |
| } |
| |
| void reset() { |
| final OptionModel defaultModel = OptionModel(); |
| _pointRounding = defaultModel.pointRounding; |
| _valleyRounding = defaultModel.valleyRounding; |
| _rotation = defaultModel.rotation; |
| _squash = defaultModel.squash; |
| _lerpAmount = defaultModel.lerpAmount; |
| _lerpTo = defaultModel.lerpTo; |
| _lerpTarget = defaultModel.lerpTarget; |
| _innerRadiusRatio = defaultModel._innerRadiusRatio; |
| _points = defaultModel.points; |
| notifyListeners(); |
| } |
| } |
| |
| class LabeledCheckbox extends StatelessWidget { |
| const LabeledCheckbox({super.key, required this.label, this.onChanged, this.value}); |
| |
| final String label; |
| final ValueChanged<bool?>? onChanged; |
| final bool? value; |
| |
| @override |
| Widget build(BuildContext context) { |
| return Row( |
| mainAxisSize: MainAxisSize.min, |
| children: <Widget>[ |
| Checkbox( |
| onChanged: onChanged, |
| value: value, |
| ), |
| Text(label), |
| ], |
| ); |
| } |
| } |
| |
| class Options extends StatefulWidget { |
| const Options(this.model, {super.key}); |
| |
| final OptionModel model; |
| |
| @override |
| State<Options> createState() => _OptionsState(); |
| } |
| |
| class _OptionsState extends State<Options> { |
| @override |
| void initState() { |
| super.initState(); |
| widget.model.addListener(_modelChanged); |
| } |
| |
| @override |
| void didUpdateWidget(Options oldWidget) { |
| super.didUpdateWidget(oldWidget); |
| if (widget.model != oldWidget.model) { |
| oldWidget.model.removeListener(_modelChanged); |
| widget.model.addListener(_modelChanged); |
| } |
| } |
| |
| @override |
| void dispose() { |
| super.dispose(); |
| widget.model.removeListener(_modelChanged); |
| } |
| |
| void _modelChanged() { |
| setState(() {}); |
| } |
| |
| double sliderValue = 0.0; |
| |
| @override |
| Widget build(BuildContext context) { |
| return Padding( |
| padding: const EdgeInsets.fromLTRB(5.0, 0.0, 5.0, 10.0), |
| child: Column( |
| mainAxisSize: MainAxisSize.min, |
| children: <Widget>[ |
| Row( |
| children: <Widget>[ |
| Expanded( |
| child: ControlSlider( |
| label: 'Point Rounding', |
| value: widget.model.pointRounding, |
| onChanged: (double value) { |
| widget.model.pointRounding = value; |
| }, |
| ), |
| ), |
| Expanded( |
| child: ControlSlider( |
| label: 'Valley Rounding', |
| value: widget.model.valleyRounding, |
| onChanged: (double value) { |
| widget.model.valleyRounding = value; |
| }, |
| ), |
| ), |
| ], |
| ), |
| Row( |
| children: <Widget>[ |
| Expanded( |
| child: ControlSlider( |
| label: 'Squash', |
| value: widget.model.squash, |
| onChanged: (double value) { |
| widget.model.squash = value; |
| }, |
| ), |
| ), |
| Expanded( |
| child: ControlSlider( |
| label: 'Rotation', |
| value: widget.model.rotation, |
| max: 360, |
| onChanged: (double value) { |
| widget.model.rotation = value; |
| }, |
| ), |
| ), |
| ], |
| ), |
| Row( |
| children: <Widget>[ |
| Expanded( |
| child: Row( |
| children: <Widget>[ |
| Expanded( |
| child: ControlSlider( |
| label: 'Points', |
| value: widget.model.points, |
| min: 2, |
| max: 20, |
| onChanged: (double value) { |
| widget.model.points = value; |
| }, |
| ), |
| ), |
| OutlinedButton( |
| child: const Text('Nearest'), |
| onPressed: () { |
| widget.model.points = widget.model.points.roundToDouble(); |
| }), |
| ], |
| ), |
| ), |
| Expanded( |
| child: ControlSlider( |
| label: 'Inner Radius', |
| value: widget.model.innerRadiusRatio, |
| onChanged: (double value) { |
| widget.model.innerRadiusRatio = value; |
| }, |
| ), |
| ), |
| ], |
| ), |
| Row( |
| children: <Widget>[ |
| Expanded( |
| flex: 2, |
| child: Padding( |
| padding: const EdgeInsetsDirectional.only(end: 8.0), |
| child: ControlSlider( |
| label: 'Lerp', |
| value: widget.model.lerpAmount, |
| onChanged: (double value) { |
| widget.model.lerpAmount = value; |
| }, |
| ), |
| ), |
| ), |
| Padding( |
| padding: const EdgeInsetsDirectional.only(start: 8.0, end: 20.0), |
| child: Column( |
| crossAxisAlignment: CrossAxisAlignment.start, |
| children: <Widget>[ |
| Row(children: <Widget>[ |
| Radio<bool>( |
| value: true, |
| groupValue: widget.model.lerpTo, |
| onChanged: (bool? value) { |
| widget.model.lerpTo = value!; |
| }), |
| const Text('To'), |
| ]), |
| Row(children: <Widget>[ |
| Radio<bool>( |
| value: false, |
| groupValue: widget.model.lerpTo, |
| onChanged: (bool? value) { |
| widget.model.lerpTo = value!; |
| }), |
| const Text('From'), |
| ]) |
| ], |
| ), |
| ), |
| Expanded( |
| child: Row( |
| children: <Widget>[ |
| Expanded( |
| child: DropdownButton<LerpTarget>( |
| items: LerpTarget.values.map<DropdownMenuItem<LerpTarget>>((LerpTarget target) { |
| return DropdownMenuItem<LerpTarget>(value: target, child: Text(target.name)); |
| }).toList(), |
| value: widget.model.lerpTarget, |
| onChanged: (LerpTarget? value) { |
| if (value == null) { |
| return; |
| } |
| widget.model.lerpTarget = value; |
| }, |
| ), |
| ), |
| ], |
| ), |
| ), |
| ], |
| ), |
| ElevatedButton( |
| onPressed: () { |
| widget.model.reset(); |
| sliderValue = 0.0; |
| }, |
| child: const Text('Reset'), |
| ), |
| ], |
| ), |
| ); |
| } |
| } |
| |
| class ControlSlider extends StatelessWidget { |
| const ControlSlider({ |
| super.key, |
| required this.label, |
| required this.value, |
| required this.onChanged, |
| this.min = 0.0, |
| this.max = 1.0, |
| }); |
| |
| final String label; |
| final double value; |
| final void Function(double value) onChanged; |
| final double min; |
| final double max; |
| |
| @override |
| Widget build(BuildContext context) { |
| return Padding( |
| padding: const EdgeInsets.all(4.0), |
| child: Row( |
| mainAxisSize: MainAxisSize.min, |
| children: <Widget>[ |
| Text(label), |
| Expanded( |
| child: Slider( |
| label: value.toStringAsFixed(1), |
| onChanged: onChanged, |
| min: min, |
| max: max, |
| value: value, |
| ), |
| ), |
| Text( |
| value.toStringAsFixed(3), |
| ), |
| ], |
| ), |
| ); |
| } |
| } |
| |
| const Color lerpToColor = Colors.red; |
| const BorderSide lerpToBorder = BorderSide(width: 5, color: lerpToColor); |
| |
| ShapeBorder? lerpBorder(StarBorder border, LerpTarget target, double t, {bool to = true}) { |
| switch (target) { |
| case LerpTarget.circle: |
| if (to) { |
| return border.lerpTo(const CircleBorder(side: lerpToBorder, eccentricity: 0.5), t); |
| } else { |
| return border.lerpFrom(const CircleBorder(side: lerpToBorder, eccentricity: 0.5), t); |
| } |
| case LerpTarget.roundedRect: |
| if (to) { |
| return border.lerpTo( |
| const RoundedRectangleBorder( |
| side: lerpToBorder, |
| borderRadius: BorderRadius.all( |
| Radius.circular(10), |
| ), |
| ), |
| t, |
| ); |
| } else { |
| return border.lerpFrom( |
| const RoundedRectangleBorder( |
| side: lerpToBorder, |
| borderRadius: BorderRadius.all( |
| Radius.circular(10), |
| ), |
| ), |
| t, |
| ); |
| } |
| case LerpTarget.rect: |
| if (to) { |
| return border.lerpTo(const RoundedRectangleBorder(side: lerpToBorder), t); |
| } else { |
| return border.lerpFrom(const RoundedRectangleBorder(side: lerpToBorder), t); |
| } |
| case LerpTarget.stadium: |
| if (to) { |
| return border.lerpTo(const StadiumBorder(side: lerpToBorder), t); |
| } else { |
| return border.lerpFrom(const StadiumBorder(side: lerpToBorder), t); |
| } |
| case LerpTarget.polygon: |
| if (to) { |
| return border.lerpTo(const StarBorder.polygon(side: lerpToBorder, sides: 4), t); |
| } else { |
| return border.lerpFrom(const StarBorder.polygon(side: lerpToBorder, sides: 4), t); |
| } |
| case LerpTarget.star: |
| if (to) { |
| return border.lerpTo(const StarBorder(side: lerpToBorder, innerRadiusRatio: .5), t); |
| } else { |
| return border.lerpFrom(const StarBorder(side: lerpToBorder, innerRadiusRatio: .5), t); |
| } |
| } |
| } |