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

news2024/11/27 18:50:36

演示:

 代码:

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});

  @override
  State<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;

  @override
  void initState() {
    super.initState();
    _load("https://c-ssl.duitang.com/uploads/item/201903/19/20190319001325_bjvzi.jpg")
        .then((value) {
      _image = value;
      update();
    });
  }

  @override
  Widget 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;

                  ///判断是否hit
                  hitDrawStep = _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);

  @override
  void 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) {
      ///构建画点与点之间的连线的path
      Path 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);
        }

        ///构建画点与点之间的连线的path
        if (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,
        );
      }
    }
  }

  @override
  bool 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.coloradmin.cn/o/1028740.html

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

相关文章

react如何根据变量渲染组件

三元运算符useMemo 第一种方法的缺点&#xff1a;其他变量更改时&#xff0c;会再次进入三元运算符,例子如下&#xff1a; //这里有一个父组件:Father { n0 ? <Father><div>{111}</div></Father> : <div>{111}</div> }第二种方法如图 …

apk获取MD5方式记录

1&#xff0c;低版本android studio 我这里是Android studio Arctic Fox 直接使用keytool -printcert -jarfile xxx.apk获取 获取得到的效果&#xff1a; 2&#xff0c;高版本android studio 在高版本下&#xff0c;按照如下图点击打开到gradle。在③步骤下直接输入signning…

《向量数据库指南》——火山引擎向量数据库对正式外开放服务

向量数据库技术全景 经过长期的内部探索和优化,抖音采用的向量数据库产品结构如下图所示:基于云基础设施,提供经过深度打磨和优化的各个引擎,提供从多模态数据写入,到向量生成,再到在线检索,以及上线后的弹性调度和监控的一整套全链路解决方案。 火山引擎向量数据库的场…

C++ 里 ++i 是原子操作吗?

1.什么是原子操作 在多线程环境下,原子操作是指不会被线程调度机制打断的操作;这种操作一旦开始,就一直运行到结束,中间不会有任何 context switch (切换到另一个线程)。 原子操作可以确保某些特定操作在多线程条件下,不会由于线程切换而导致数据污染。比如,对一个变量的读/写…

ASEMI快恢复二极管S1FD40A180H参数,S1FD40A180H应用

编辑-Z S1FD40A180H参数描述&#xff1a; 型号&#xff1a;S1FD40A180H 最大直流反向电压VR&#xff1a;1800V 最大工作峰值反向电压VRWM&#xff1a;1440V 最大平均正向电流IF&#xff1a;40A 非重复正向浪涌电流IFSM&#xff1a;500A 操作和储存温度范围TJ ,TSTG&…

洞察2023:中国心室辅助装置行业竞争格局及市场份额

本文核心数据&#xff1a;代表性企业排名 ; 代表性企业优势分析等 1、中国心室辅助装置行业竞争梯队 人工心脏 ( Artificial Heart, AH ) 是机械辅助类器械的代表&#xff0c;用于替代或辅助心脏泵血功能。按照功能可分为心室辅助装置 ( Ventricular Assist Device&#xff0…

Python与数据分析--每天绘制Matplotlib库实例图片3张-第1天

目录 1.实例1--Bar color demo 2.实例2--Bar Label Demo 3.实例3--Grouped bar chart with labels 1.实例1--Bar color demo import matplotlib.pyplot as plt # 支持中文 plt.rcParams[font.sans-serif] [SimHei] # 用来正常显示中文标签 plt.rcParams[axes.unicode_minus…

Node.js环境安装与服务设置,结合内网穿透随时随地公网访问!

文章目录 前言1.安装Node.js环境2.创建node.js服务3. 访问node.js 服务4.内网穿透4.1 安装配置cpolar内网穿透4.2 创建隧道映射本地端口 5.固定公网地址 前言 Node.js 是能够在服务器端运行 JavaScript 的开放源代码、跨平台运行环境。Node.js 由 OpenJS Foundation&#xff0…

spring boot +vue 博客系统,开源的资源网站

spring boot vue 博客系统&#xff0c;开源的资源网站&#xff08;Aurora前后端分离博客) 体验地址&#xff1a;http://blog.tlzcf.vip/ 相关技术 前端&#xff1a; 样式来自于&#xff1a;hexo的aurora主题基础框架&#xff1a;vue3(前台) vue2(后台)状态管理&#xff1a;…

蓝桥杯每日一题2023.9.21

蓝桥杯2021年第十二届省赛真题-异或数列 - C语言网 (dotcpp.com) 题目描述 Alice 和 Bob 正在玩一个异或数列的游戏。初始时&#xff0c;Alice 和 Bob 分别有一个整数 a 和 b&#xff0c;有一个给定的长度为 n 的公共数列 X1, X2, , Xn。 Alice 和 Bob 轮流操作&#xff0…

安科瑞为工业能效提升行动计划提供EMS解决方案-安科瑞黄安南

摘要: 2022年6月29日工信部、发改委、财政部、生态环境部、国资委、市场监管总局六部门联合下发《关于印发工业能效提升行动计划的通知》&#xff08;工信部联节〔2022〕76号&#xff0c;以下简称《行动计划》&#xff09;&#xff0c;主要目的是为了提高工业领域能源利用效率&…

共享门店:一种创新的商业模式

你是否想过&#xff0c;如果你的门店可以和你的客户、好友、合作伙伴共享经营权和收益权&#xff0c;你的门店会有多大的发展空间和盈利能力&#xff1f;你是否想过&#xff0c;如果你的门店可以利用互联网、人工智能、物联网等先进技术&#xff0c;你的门店会有多高的效率和竞…

MySQL 学习笔记(基础)

首先解释数据库DataBase&#xff08;DB&#xff09;&#xff1a;即存储数据的仓库&#xff0c;数据经过有组织的存储 数据库管理系统DataBase Management System&#xff08;DBMS&#xff09;&#xff1a;管理数据库的软件 SQL&#xff08;Structured Query Language&#xf…

GIT使用需知,哪些操作会导致本地代码变动

系列文章目录 手把手教你安装Git&#xff0c;萌新迈向专业的必备一步 GIT命令只会抄却不理解&#xff1f;看完原理才能事半功倍&#xff01; 常用GIT命令详解&#xff0c;手把手让你登堂入室 GIT实战篇&#xff0c;教你如何使用GIT可视化工具 GIT使用需知&#xff0c;哪些操作…

手机全自动无人直播系统,成为商家实景无人直播带货好帮手!

商家手机无人直播系统最近太火爆了&#xff0c;那么&#xff0c;这个产品究竟是什么呢&#xff1f;全自动无人直播系统是一款手机自动直播软件&#xff0c;目地在于帮助广大商家和企业实现无人直播卖货&#xff0c;从而解放双手、降低人工干预的需求。 当然&#xff0c;无人直播…

ModStartCMS v7.3.0 富文本MP3支持,后台组件优化

ModStart 是一个基于 Laravel 模块化极速开发框架。模块市场拥有丰富的功能应用&#xff0c;支持后台一键快速安装&#xff0c;让开发者能快的实现业务功能开发。 系统完全开源&#xff0c;基于 Apache 2.0 开源协议&#xff0c;免费且不限制商业使用。 功能特性 丰富的模块市…

抗锯齿的线

抗锯齿的线 右下角的时候h是0,到顶部 h是1&#xff0c;然后中间y相距4个像素&#xff0c;那dy就是0.25 如果让h abs(fract(h - 0.5) - 0.5) 中间一行0.5&#xff0c;第一行 第三行都是0.25&#xff0c;两端都是0 根据插值来看 这里是 如果用h/dy 那么第一行以上&#xff0…

机器学习入门教学——损失函数(极大似然估计法)

1、前言 我们在训练神经网络时&#xff0c;最常用到的方法就是梯度下降法。在了解梯度下降法前&#xff0c;我们需要了解什么是损失(代价)函数。所谓求的梯度&#xff0c;就是损失函数的梯度。如果不知道什么是梯度下降的&#xff0c;可以看一下这篇文章&#xff1a;机器学习入…

黑马JVM总结(十八)

&#xff08;1&#xff09;G1_FullGC的概念辨析 SerialGC&#xff1a;串行的&#xff0c;ParallelGC&#xff1a;并行的 &#xff0c;CMS和G1都是并发的 这几种垃圾回收器的新生代回收机制时相同的&#xff0c;SerialGC和ParalledGC&#xff1a;老年代内存不足触发的叫FullGC…

【redis总结】

文章目录 1、redis简介2、为什么要选择redis做缓存3、数据结构4、redis多线程模型redis6.0的变化 5、redis持久化AOF的实现过程RDB的实现过程 6、redis集群的搭建7、 redis过期删除和淘汰策略8、redis的内存淘汰策略 1、redis简介 Redis&#xff08;Remote Dictionary Server&…