Flutter实现PS钢笔工具,实现高精度抠图的效果。

演示:

 代码:

import 'dart:ui';import 'package:flutter/material.dart' hide Image;
import 'package:flutter/services.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:kq_flutter_widgets/widgets/animate/stack.dart';
import 'package:kq_flutter_widgets/widgets/button/kq_small_button.dart';
import 'package:kq_flutter_widgets/widgets/update/update_view.dart';///抠图软件原型
class DrawPathTest extends StatefulWidget {const DrawPathTest({super.key});@overrideState<StatefulWidget> createState() => DrawPathTestState();
}class DrawPathTestState extends State<DrawPathTest> {///是否绑定左右操作点,即操作一个点,另一个点自动计算static bool isBind = true;///击中范围半径static double hitRadius = 5;///绘制区域state持有UpdateViewState? state;///背景图Image? _image;///历史步骤存储KqStack stackHistory = KqStack();///回收站步骤存储KqStack stackRecycleBin = KqStack();///绘制步骤集合List<Step> drawList = [];///手指按下时点击的控制点的位置缓存Step? hitControlStep;///手指按下时点击的画线点的位置缓存Step? hitDrawStep;///闭合绘制完成状态,不再添加点bool drawFinish = false;@overridevoid initState() {super.initState();_load("https://c-ssl.duitang.com/uploads/item/201903/19/20190319001325_bjvzi.jpg").then((value) {_image = value;update();});}@overrideWidget build(BuildContext context) {return Column(children: [Expanded(child: LayoutBuilder(builder: (c, lc) {return Container(color: Colors.white60,child: Listener(onPointerDown: (v) {Offset src = v.localPosition;///判断是否hithitDrawStep = _isHitDrawPoint(src);if (!drawFinish) {if (hitDrawStep != null && hitDrawStep!.isFirst) {_add(src, isLast: true);drawFinish = true;} else {hitControlStep = _isHitControlPoint(src);hitControlStep ??= _add(src);}update();} else {hitControlStep = _isHitControlPoint(src);}},onPointerMove: (v) {if (hitDrawStep != null) {_update(hitDrawStep!, v.localPosition);update();} else if (hitControlStep != null) {_update(hitControlStep!, v.localPosition);update();}},child: UpdateView(build: (UpdateViewState state) {this.state = state;return CustomPaint(size: Size(lc.maxWidth, lc.maxHeight),painter: TestDraw(_image, drawList),);},),),);}),),Row(children: [SizedBox(width: 20.r),Expanded(child: KqSmallButton(title: "撤销",onTap: (disabled) {_undo();update();},),),SizedBox(width: 20.r),Expanded(child: KqSmallButton(title: "重做",onTap: (disabled) {_redo();update();},),),SizedBox(width: 20.r),Expanded(child: KqSmallButton(title: "选择",onTap: (disabled) {_select();update();},),),SizedBox(width: 20.r),Expanded(child: KqSmallButton(title: "反选",onTap: (disabled) {_invert();update();},),),SizedBox(width: 20.r),Expanded(child: KqSmallButton(title: "删除",onTap: (disabled) {_delete();update();},),),SizedBox(width: 20.r),],),SizedBox(height: 20.r),],);}///更新绘制区域update() {state?.update();}///添加点Step _add(Offset offset, {bool isLast = false}) {Step step = Step(offset, offset, offset);step.isLast = isLast;if (drawList.isEmpty) {step.isFirst = true;}//添加到历史stackHistory.push(step);//添加到绘制列表drawList.add(step);//清除垃圾箱stackRecycleBin.clear();return step;}///判断是否点击在控制点上Step? _isHitControlPoint(Offset src) {for (Step step in drawList) {if (_distance(step.pointRight, src) < hitRadius) {step.hitPointType = PointType.pointRight;return step;} else if (_distance(step.pointLeft, src) < hitRadius) {step.hitPointType = PointType.pointLeft;return step;}}return null;}///判断是否点击在连接点上Step? _isHitDrawPoint(Offset src) {for (Step step in drawList) {if (_distance(step.point, src) < hitRadius) {step.hitPointType = PointType.point;return step;}}return null;}///更新点信息_update(Step hitStep, Offset target) {if (hitStep.hitPointType == PointType.pointRight) {hitStep.pointRight = target;if (isBind) {hitStep.pointLeft = hitStep.point.scale(2, 2) - target;}} else if (hitStep.hitPointType == PointType.pointLeft) {hitStep.pointLeft = target;if (isBind) {hitStep.pointRight = hitStep.point.scale(2, 2) - target;}} else if (hitStep.hitPointType == PointType.point) {hitStep.pointLeft = hitStep.pointLeft - hitStep.point + target;hitStep.pointRight = hitStep.pointRight - hitStep.point + target;hitStep.point = target;}}///两点距离double _distance(Offset one, Offset two) {return (one - two).distance;}///撤销、后退_undo() {Step? step = stackHistory.pop();if (step != null) {drawList.remove(step);stackRecycleBin.push(step);}}///重做、前进_redo() {Step? step = stackRecycleBin.pop();if (step != null) {drawList.add(step);stackHistory.push(step);}}///选择、完成_select() {}///反选、完成_invert() {}///删除_delete() {}///加载图片Future<Image> _load(String url) async {ByteData data = await NetworkAssetBundle(Uri.parse(url)).load(url);Codec codec = await instantiateImageCodec(data.buffer.asUint8List());FrameInfo fi = await codec.getNextFrame();return fi.image;}
}class TestDraw extends CustomPainter {static double width = 260;static double width1 = 50;static double height1 = 100;///绘制集合final List<Step> draw;///背景图片final Image? image;Step? tempStep;Step? tempFirstStep;TestDraw(this.image, this.draw);@overridevoid paint(Canvas canvas, Size size) {///绘制背景if (image != null) {canvas.drawImageRect(image!,Rect.fromLTRB(0,0,image!.width.toDouble(),image!.height.toDouble(),),Rect.fromLTRB(width1,height1,width + width1,width * image!.height / image!.width + height1,),Paint(),);}if (draw.isNotEmpty) {///构建画点与点之间的连线的pathPath path = Path();///绘制点和线for (int i = 0; i < draw.length; i++) {Step step = draw[i];if (!step.isLast) {canvas.drawCircle(step.point, 4.r, Paint()..color = Colors.red);canvas.drawCircle(step.pointLeft, 4.r, Paint()..color = Colors.purple);canvas.drawCircle(step.pointRight, 4.r, Paint()..color = Colors.purple);///画控制点和连线点之间的线段canvas.drawLine(step.point,step.pointLeft,Paint()..color = Colors.green..style = PaintingStyle.stroke);canvas.drawLine(step.point,step.pointRight,Paint()..color = Colors.green..style = PaintingStyle.stroke);}///构建画点与点之间的连线的pathif (step.isLast) {if (tempFirstStep != null && tempStep != null) {path.cubicTo(tempStep!.pointRight.dx,tempStep!.pointRight.dy,tempFirstStep!.pointLeft.dx,tempFirstStep!.pointLeft.dy,tempFirstStep!.point.dx,tempFirstStep!.point.dy,);}} else {//处理初始点if (step.isFirst) {tempFirstStep = step;path.moveTo(step.point.dx, step.point.dy);}if (tempStep != null) {path.cubicTo(tempStep!.pointRight.dx,tempStep!.pointRight.dy,step.pointLeft.dx,step.pointLeft.dy,step.point.dx,step.point.dy,);}}tempStep = step;}if (draw.length >= 2) {canvas.drawPath(path,Paint()..color = Colors.red..style = PaintingStyle.stroke..strokeWidth = 1.5,);}}}@overridebool shouldRepaint(covariant CustomPainter oldDelegate) {return true;}
}class Step {///线条连接点Offset point;///右控制点Offset pointRight;///左控制点(起始点没有左控制点的)Offset pointLeft;///是否选中了点的类型PointType hitPointType = PointType.pointRight;///是否是第一个控制点bool isFirst = false;///是否是最后一个控制点bool isLast = false;Step(this.point,this.pointRight,this.pointLeft,);
}///点类型
enum PointType {///线条连接点point,///右控制点pointRight,///左控制点pointLeft
}

stack代码:

///栈,先进后出
class KqStack<T> {final List<T> _stack = [];///入栈push(T obj) {_stack.add(obj);}///出栈T? pop() {if (_stack.isEmpty) {return null;} else {return _stack.removeLast();}}///栈长度length() {return _stack.length;}///清除栈clear() {_stack.clear();}
}

主要思路:

更具手指点击屏幕的位置,记录点击的位置,并生成绘制点和两个控制点,手指拖动控制点时,动态刷新控制点位置,然后利用flutter绘制机制,在canvas上根据点的位置和控制点的位置绘制三阶贝塞尔曲线,实现钢笔工具效果。具体实现可以看代码,有注释,逻辑应该还算清晰。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/86659.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【Vue】快速入门和生命周期

目录 前言 一、vue的介绍 1. Vue.js是什么&#xff1f; 2. 库和框架的区别 3.基本概念和用法&#xff1a; 二、MVVM的介绍 1. 什么是MVVM&#xff1f; 2. MVVM的组成部分 3. MVVM的工作流程 4. MVVM的优势 5. MVVM的应用场景 三、vue实例 1.模板语法&#xff1a; …

智慧公厕是提升公共厕所管理服务能力的创新举措

在城市化进程加速的今天&#xff0c;公共厕所的管理问题成为让人头疼的难题。随着智慧科技的发展&#xff0c;智慧公厕应运而生&#xff0c;为提升公共厕所综合管理服务能力提供了新思路和解决方案。本文将以智慧公厕领先厂家广州中期科技有限公司&#xff0c;大量精品案例项目…

卡尔曼滤波(Kalman Filter)原理浅析-数学理论推导-4

目录 前言数学理论推导1. 直观理解与二维实例2. EKF3. 补充知识-线性化结语参考 前言 最近项目需求涉及到目标跟踪部分&#xff0c;准备从 DeepSORT 多目标跟踪算法入手。DeepSORT 中涉及的内容有点多&#xff0c;以前也就对其进行了简单的了解&#xff0c;但是真正去做发现总是…

搞定ESD(一):静电放电测试标准解析

文章目录 一、基本术语与定义1.1 基本定义1.2 重要基本术语 二、静电放电发生器介绍2.1 静电放电发生器的特性&#xff1a;通用规范【GB/T17626.2-2018 标准】2.2 ESD 放电发生器电极规格要求&#xff1a;通用规范【GB/T17626.2-2018 标准】2.3 放电回路电缆的要求&#xff1a;…

黑马JVM总结(十四)

&#xff08;1&#xff09;分代回收_1 Java虚拟机都是结合前面几种算法&#xff0c;让他们协同工作&#xff0c;具体实现是虚拟机里面一个叫做分代的垃圾回收机制&#xff0c;把我们堆内存大的区域划分为两块新生代、老年代 新生代有划分为伊甸园、幸存区Form、幸存区To 为什…

进程同步与互斥

目录 进程同步与互斥&#xff08;1&#xff09; 第一节、进程间相互作用 一、相关进程和无关进程 二、与时间有关的错误 第二节、进程同步与互斥 一、进程的同步 二、进程的互斥 三、临界区 进程同步与互斥&#xff08;2&#xff09; 三、信号量与P、V操作的物理含义…

防泄密软件推荐(数据防泄漏软件好用榜前五名)

在当今的数字化时代&#xff0c;数据已经成为企业最宝贵的资产之一。企业需要依赖数据来驱动业务决策、提高运营效率和创新产品。然而&#xff0c;随着数据量的不断增长&#xff0c;数据安全问题也日益凸显。企业需要采取有效的措施来保护敏感数据&#xff0c;防止信息泄露给竞…

算法通关村第16关【青铜】| 滑动窗口思想

1. 滑动窗口的基本思想 一句话概括就是两个快慢指针维护的一个会移动的区间 固定大小窗口&#xff1a;求哪个窗口元素最大、最小、平均值、和最大、和最小 可变大小窗口&#xff1a;求一个序列里最大、最小窗口是什么 2. 两个入门题 &#xff08;1&#xff09;子数组最大平…

使用FastChat部署Baichuan2

1. 引言 近来&#xff0c;大型语言模型的市场需求呈现出蓬勃发展的态势。然而&#xff0c;仅仅掌握模型的数据准备和训练是不够的&#xff0c;模型的部署方法也变得至关重要。在这篇文章中&#xff0c;我们将以Baichuan2为例&#xff0c;利用FastChat进行模型部署的实战操作。…

使用亚马逊云服务器在 G4 实例上运行 Android 应用程序

随着 Android 应用程序和游戏变得越来越丰富&#xff0c;其中有些甚至比 PC 上的软件更易于使用和娱乐&#xff0c;因此许多人希望能够在云上运行 Android 游戏或应用程序&#xff0c;而在 EC2 实例上运行 Android 的解决方案可以让开发人员更轻松地测试和运行 Android 应用程序…

MySQL 笔试——多表连接查询

一、&#xff08;左、右和全&#xff09;连接概念 内连接&#xff1a; 假设A和B表进行连接&#xff0c;使用内连接的话&#xff0c;凡是A表和B表能够匹配上的记录查询出来。A和B两张表没有主付之分&#xff0c;两张表是平等的。 关键字&#xff1a;inner join on 语句&#xf…

Spring之依赖注入源码解析

基于Autowired的依赖注入底层原理 基于Resource注解底层工作流程图&#xff1a; 1 Spring中到底有几种依赖注入的方式&#xff1f; 首先分两种&#xff1a; 手动注入 自动注入 1.1 手动注入 在XML中定义Bean时&#xff0c;就是手动注入&#xff0c;因为是程序员手动给某…

MySQL高级语句 Part2(视图表 +存储过程+条件语句+循环语句)

这里写目录标题 一、视图表 create view1.1 视图表概述1.2 视图表能否修改&#xff1f;&#xff08;面试题&#xff09;1.3 基本语法1.3.1 创建1.3.2 查看1.3.3 删除 1.4 通过视图表求无交集值 二、case语句三、空值(null) 和 无值( ) 的区别四、正则表达式4.1 基本语法和匹配模…

css,环形

思路&#xff1a; 1.先利用conic-gradient属性画一个圆&#xff0c;然后再叠加 效果图 <template><div class"ring"><div class"content"><slot></slot></div></div> </template> <script> import …

【力扣每日一题】2023.9.24 LRU缓存

目录 题目&#xff1a; 示例&#xff1a; 分析&#xff1a; 代码&#xff1a; 题目&#xff1a; 示例&#xff1a; 分析&#xff1a; 这又是一道程序设计类的题目&#xff0c;要我们实现LRU缓存的get和put操作。 简单说一下LRU缓存是什么&#xff0c;在我看来就是实用主义…

【李沐深度学习笔记】矩阵计算(2)

课程地址和说明 线性代数实现p4 本系列文章是我学习李沐老师深度学习系列课程的学习笔记&#xff0c;可能会对李沐老师上课没讲到的进行补充。 本节是第二篇 矩阵计算 矩阵的导数运算 此处参考了视频&#xff1a;矩阵的导数运算 为了方便看出区别&#xff0c;我将所有的向量…

VSCode 配置 Lua 开发环境(清晰明了)

概述 由于 AutoJS 学得已经差不多了&#xff0c;基本都会了&#xff0c;现在开始向其他游戏脚本框架进发&#xff0c; Lua 语言很强大&#xff0c;就不多说&#xff0c; 按键精灵、触动精灵等等都是用该语言编程脚本的&#xff0c;由于按键精灵、触动精灵 和 AutoJS 类似,不是…

基于Xml方式Bean的配置-初始化方法和销毁方法

SpringBean的配置详解 Bean的初始化和销毁方法配置 Bean在被实例化后&#xff0c;可以执行指定的初始化方法完成一些初始化的操作&#xff0c;Bean在销毁之前也可以执行指定的销毁方法完成一些操作&#xff0c;初始化方法名称和销毁方法名称通过 <bean id"userService…

后置处理 Bean

目录 ​编辑一、后置处理 Bean 1、后置处理 Bean 的运行原理分析 2、BeanPostProcessor 的开发步骤 &#xff08;1&#xff09;实现 BeanPostProcessor 接口 &#xff08;2&#xff09;Spring 的配置文件配置 &#xff08;3&#xff09;细节 一、后置处理 Bean BeanPost…

什么是IoT数字孪生?

数字孪生是资产或系统的实时虚拟模型&#xff0c;它使用来自连接的物联网传感器的数据来创建数字表示。数字孪生允许您从任何地方实时监控设备、资产或流程。数字孪生用于多种目的&#xff0c;例如分析性能、监控问题或在实施之前运行测试。从物联网数字孪生中获得的见解使用户…