Flutter贝塞尔曲线绘制插件bezier_kit的使用

Flutter贝塞尔曲线绘制插件bezier_kit的使用

bezier_kit 是一个用Dart编写的全面的贝塞尔路径库。

关于

bezier_kit 是从原始的Swift版本 BezierKit 手动转换而来的。它基于2022年6月28日(版本0.15.0)的 BezierKit 分支。

特性

  • ✅ 构造线段、二次和三次贝塞尔曲线
  • ✅ 确定曲线上任意位置、导数和法线
  • ✅ 使用Legendre-Gauss求积计算曲线长度
  • ✅ 计算曲线交点,并精确到任意精度
  • ✅ 确定边界框和极值
  • ✅ 定位到给定点最近的曲线位置
  • ✅ 拆分曲线为子曲线
  • ✅ 偏移和轮廓曲线
  • ❌ 全面的单元和集成测试覆盖
  • ❌ 完整的文档

安装

Dart

dart pub add bezier_kit

Flutter

flutter pub add bezier_kit

使用

构造与绘制曲线

bezier_kit 支持三次贝塞尔曲线 (CubicCurve) 和二次贝塞尔曲线 (QuadraticCurve),以及线段 (LineSegment),每种类型都实现了 BezierCurve 协议,该协议包含大多数API功能。

import 'package:bezier_kit/bezier_kit.dart';

final curve = CubicCurve(
  p0: Point(x: 100, y: 25),
  p1: Point(x: 10, y: 90),
  p2: Point(x: 110, y: 100),
  p3: Point(x: 150, y: 195),
);

final canvas: Canvas = ...        // 你的绘图上下文
final draw = Draw(canvas);
draw.drawSkeleton(curve: curve);  // 绘制控制点
draw.drawCurve(curve: curve);     // 绘制曲线本身

曲线相交

intersectionsWithCurve(curve) 方法可以确定 selfcurve 之间的每个交点,返回一个 Intersection 对象数组。每个交点有两个字段:t1 表示 self 在交点处的 t 值,t2 表示 curve 在交点处的 t 值。你可以通过传递对应的 t 值来计算交点的坐标。

三次曲线可能会自交,可以通过调用 selfIntersections() 方法来确定。

final intersections = curve1.intersectionsWithCurve(curve2);
final points = intersections.map((i) => curve1.point(at: i.t1));

draw.drawCurve(curve: curve1);
draw.drawCurve(curve: curve2);
for (final p in points) {
  draw.drawPoint(origin: p);
}

曲线分割

split(from:, to:) 方法可以在给定的 t 值范围内生成子曲线。split(at:) 方法可以用于生成由单个 t 值分割出的左子曲线和右子曲线。

draw.setColor(color: Draw.lightGrey);
draw.drawSkeleton(curve: curve);
draw.drawCurve(curve: curve);
final subcurve = curve.split(from: 0.25, to: 0.75); // 或者尝试 (leftCurve, rightCurve) = curve.split(at:)
draw.setColor(color: Draw.red);
draw.drawCurve(curve: subcurve);
draw.drawCircle(center: curve.point(at: 0.25), radius: 3);
draw.drawCircle(center: curve.point(at: 0.75), radius: 3);

确定边界框

final boundingBox = curve.boundingBox;
draw.drawSkeleton(context, curve: curve);
draw.drawCurve(context, curve: curve);
draw.setColor(context, color: Draw.pinkish);
draw.drawBoundingBox(context, boundingBox: curve.boundingBox);

更多

bezier_kit 是一个功能强大的库,拥有大量功能。目前最好的方式是构建示例 Flutter 应用并检查提供的每个演示。

测试

bezier_kit 包含从原始源代码转换过来的整个测试套件,从Swift转换为Dart。

要运行测试套件,请执行:

dart test

许可证

bezier_kit 是在MIT许可证下发布的。详情请参阅 LICENSE文件


示例代码

以下是完整的示例代码,展示了如何使用 bezier_kit 插件。

// Copyright (c) 2023-2024 Guilherme Lepsch. All rights reserved. Use of
// this source code is governed by MIT license that can be found in the
// [LICENSE file](https://github.com/lepsch/bezier_kit/blob/main/LICENSE).

import 'package:bezier_kit/bezier_kit.dart';
import 'package:example/demos.dart';
import 'package:example/draw.dart';
import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  [@override](/user/override)
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'bezier_kit Flutter Demo',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key});

  [@override](/user/override)
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  var _demoIndex = 0;
  final _draggables = all[0].cubicControlPoints.toList();
  var _useQuadratic = false;
  var _isPanning = false;
  Point? _lastInputLocation;

  void resetDraggables() {
    _draggables.clear();
    final demo = all[_demoIndex];
    (_useQuadratic ? demo.quadraticControlPoints : demo.cubicControlPoints)
        .forEach(_draggables.add);
  }

  [@override](/user/override)
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: Text("bezier_kit Flutter Demo"),
      ),
      body: ConstrainedBox(
        constraints: BoxConstraints.expand(),
        child: LayoutBuilder(builder: (context, constraints) {
          final dx = (constraints.maxWidth - intrinsicContentSize.width) / 2;
          final dy = (constraints.maxHeight - intrinsicContentSize.height) / 2;
          return MouseRegion(
            onExit: (event) => setState(() => _lastInputLocation = null),
            onHover: (event) => setState(() => _lastInputLocation = event.localPosition.toPoint()),
            child: Stack(
              children: [
                Container(
                  color: Colors.white,
                ),
                CustomPaint(
                  size: Size(constraints.maxWidth, constraints.maxHeight),
                  painter: Painter(
                    _demoIndex,
                    _draggables,
                    _useQuadratic,
                    _lastInputLocation,
                  ),
                ),
                for (final (i, draggable) in _draggables.indexed)
                  Positioned(
                      left: draggable.x + dx - draggableWidth / 2,
                      top: draggable.y + dy - draggableWidth / 2,
                      child: MouseRegion(
                        cursor: _isPanning
                            ? SystemMouseCursors.grabbing
                            : SystemMouseCursors.grab,
                        child: GestureDetector(
                          onPanStart: (_) => setState(() => _isPanning = true),
                          onPanEnd: (_) => setState(() => _isPanning = false),
                          onPanUpdate: (details) => setState(
                              () => _draggables[i] += details.delta.toPoint()),
                          child: Container(
                            width: 20,
                            height: 20,
                            color: Colors.transparent,
                          ),
                        ),
                      ))
              ],
            ),
          );
        }),
      ),
      bottomNavigationBar: IntrinsicHeight(
        child: Row(
          mainAxisSize: MainAxisSize.min,
          mainAxisAlignment: MainAxisAlignment.spaceBetween,
          children: [
            IntrinsicWidth(
              child: Column(
                mainAxisSize: MainAxisSize.min,
                crossAxisAlignment: CrossAxisAlignment.stretch,
                children: [
                  ListTile(
                    title: const Text('Quadratic'),
                    leading: Radio<bool>(
                      value: true,
                      groupValue: _useQuadratic,
                      onChanged: (value) => setState(() {
                        _useQuadratic = value!;
                        resetDraggables();
                      }),
                    ),
                  ),
                  ListTile(
                    title: const Text('Cubic'),
                    leading: Radio<bool>(
                      value: false,
                      groupValue: _useQuadratic,
                      onChanged: (value) => setState(() {
                        _useQuadratic = value!;
                        resetDraggables();
                      }),
                    ),
                  ),
                ],
              ),
            ),
            DropdownButton<int>(
              value: _demoIndex,
              icon: const Icon(Icons.arrow_downward),
              onChanged: (value) {
                setState(() {
                  _demoIndex = value!;
                  resetDraggables();
                });
              },
              items: all.indexed
                  .map<DropdownMenuItem<int>>((demo) => DropdownMenuItem<int>(
                      value: demo.$1, child: Text(demo.$2.title)))
                  .toList(),
            )
          ],
        ),
      ),
    );
  }
}

class Painter extends CustomPainter {
  final int demoIndex;
  final bool useQuadratic;
  final List<Point> draggables;
  final Point? lastInputLocation;
  const Painter(this.demoIndex, this.draggables, this.useQuadratic, this.lastInputLocation);

  [@override](/user/override)
  void paint(Canvas canvas, Size size) {
    final demo = all[demoIndex];

    // 将曲线居中
    final dx = (size.width - intrinsicContentSize.width) / 2;
    final dy = (size.height - intrinsicContentSize.height) / 2;
    canvas.translate(dx, dy);

    final curve = draggables.isNotEmpty
        ? useQuadratic
            ? QuadraticCurve.fromList(points: draggables)
            : CubicCurve.fromList(draggables)
        : null;

    demo.drawFunction(
      canvas,
      DemoState(
        lastInputLocation: lastInputLocation != null
            ? Point(x: lastInputLocation!.x - dx, y: lastInputLocation!.y - dy)
            : null,
        quadratic: useQuadratic,
        curve: curve,
      ),
    );
  }

  [@override](/user/override)
  bool shouldRepaint(CustomPainter oldDelegate) {
    return false;
  }
}

const intrinsicContentSize = Size(200, 210);
const draggableWidth = 20;

更多关于Flutter贝塞尔曲线绘制插件bezier_kit的使用的实战教程也可以访问 https://www.itying.com/category-92-b0.html

1 回复

更多关于Flutter贝塞尔曲线绘制插件bezier_kit的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html


bezier_kit 是一个用于在 Flutter 中绘制贝塞尔曲线的插件。它提供了灵活的方式来绘制二次贝塞尔曲线、三次贝塞尔曲线等,并且可以轻松地自定义曲线的样式和动画效果。以下是如何使用 bezier_kit 插件来绘制贝塞尔曲线的基本步骤。

1. 添加依赖

首先,你需要在 pubspec.yaml 文件中添加 bezier_kit 的依赖:

dependencies:
  flutter:
    sdk: flutter
  bezier_kit: ^0.1.0  # 请检查最新版本

然后运行 flutter pub get 来获取依赖。

2. 导入包

在你的 Dart 文件中导入 bezier_kit 包:

import 'package:bezier_kit/bezier_kit.dart';

3. 绘制贝塞尔曲线

你可以使用 BezierPainter 来绘制贝塞尔曲线。以下是一个简单的例子:

import 'package:flutter/material.dart';
import 'package:bezier_kit/bezier_kit.dart';

class BezierCurveExample extends StatelessWidget {
  [@override](/user/override)
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Bezier Curve Example'),
      ),
      body: Center(
        child: CustomPaint(
          size: Size(300, 300),
          painter: BezierCurvePainter(),
        ),
      ),
    );
  }
}

class BezierCurvePainter extends CustomPainter {
  [@override](/user/override)
  void paint(Canvas canvas, Size size) {
    final paint = Paint()
      ..color = Colors.blue
      ..strokeWidth = 4.0
      ..style = PaintingStyle.stroke;

    // 定义控制点
    final startPoint = Offset(50, 150);
    final controlPoint1 = Offset(100, 50);
    final controlPoint2 = Offset(200, 250);
    final endPoint = Offset(250, 150);

    // 创建三次贝塞尔曲线
    final cubicBezier = CubicBezier(
      startPoint: startPoint,
      controlPoint1: controlPoint1,
      controlPoint2: controlPoint2,
      endPoint: endPoint,
    );

    // 绘制贝塞尔曲线
    cubicBezier.paint(canvas, paint);
  }

  [@override](/user/override)
  bool shouldRepaint(CustomPainter oldDelegate) => true;
}

void main() {
  runApp(MaterialApp(
    home: BezierCurveExample(),
  ));
}

4. 运行应用

运行你的 Flutter 应用程序,你应该会看到一个绘制了三次贝塞尔曲线的界面。

5. 自定义曲线

你可以通过调整 startPointcontrolPoint1controlPoint2endPoint 的位置来改变曲线的形状。你也可以使用 QuadraticBezier 来绘制二次贝塞尔曲线。

// 创建二次贝塞尔曲线
final quadraticBezier = QuadraticBezier(
  startPoint: startPoint,
  controlPoint: controlPoint1,
  endPoint: endPoint,
);

// 绘制二次贝塞尔曲线
quadraticBezier.paint(canvas, paint);

6. 添加动画

你可以结合 Flutter 的动画系统来创建动态的贝塞尔曲线。例如,可以使用 AnimationControllerTween 来动态改变控制点的位置,从而让曲线动起来。

class AnimatedBezierCurve extends StatefulWidget {
  [@override](/user/override)
  _AnimatedBezierCurveState createState() => _AnimatedBezierCurveState();
}

class _AnimatedBezierCurveState extends State<AnimatedBezierCurve>
    with SingleTickerProviderStateMixin {
  AnimationController _controller;
  Animation<Offset> _animation;

  [@override](/user/override)
  void initState() {
    super.initState();
    _controller = AnimationController(
      duration: const Duration(seconds: 2),
      vsync: this,
    )..repeat(reverse: true);

    _animation = Tween<Offset>(
      begin: Offset(100, 50),
      end: Offset(200, 250),
    ).animate(_controller);
  }

  [@override](/user/override)
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  [@override](/user/override)
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Animated Bezier Curve'),
      ),
      body: Center(
        child: AnimatedBuilder(
          animation: _animation,
          builder: (context, child) {
            return CustomPaint(
              size: Size(300, 300),
              painter: AnimatedBezierCurvePainter(_animation.value),
            );
          },
        ),
      ),
    );
  }
}

class AnimatedBezierCurvePainter extends CustomPainter {
  final Offset controlPoint;

  AnimatedBezierCurvePainter(this.controlPoint);

  [@override](/user/override)
  void paint(Canvas canvas, Size size) {
    final paint = Paint()
      ..color = Colors.blue
      ..strokeWidth = 4.0
      ..style = PaintingStyle.stroke;

    final startPoint = Offset(50, 150);
    final endPoint = Offset(250, 150);

    final cubicBezier = CubicBezier(
      startPoint: startPoint,
      controlPoint1: controlPoint,
      controlPoint2: Offset(200, 250),
      endPoint: endPoint,
    );

    cubicBezier.paint(canvas, paint);
  }

  [@override](/user/override)
  bool shouldRepaint(CustomPainter oldDelegate) => true;
}

void main() {
  runApp(MaterialApp(
    home: AnimatedBezierCurve(),
  ));
}
回到顶部