Flutter 像素编辑器#05 | 缩放与平移


theme: cyanosis

本系列,将通过 Flutter 实现一个全平台的像素编辑器应用。源码见开源项目 【pix_editor】。在前三篇中,我们已经完成了一个简易的图像编辑器,并且简单引入了图层的概念,支持切换图层显示不同的像素画面。

  • 《Flutter 像素编辑器#01 | 像素网格》
  • 《Flutter 像素编辑器#02 | 配置编辑》
  • 《Flutter 像素编辑器#03 | 像素图层》
  • 《Flutter 像素编辑器#04 | 导入导出图像》

0.本文目的

之前已经实现了像素编辑器的基本功能,但是目前绘制的区域是固定大小。这样在行列数非常大时,就会导致绘制格非常小,不便于绘制。所以希望布局区域可以向 Photoshop 一样,能够缩放和平移,让用户更自由地绘制。

jvideo

其中有几个个关键的难点:

  1. 如何通过手势、鼠标操作,触发缩放和平移事件。
  2. 绘制区域进行缩放平移变换后,落点在单元格内的校验逻辑如何适应。
  3. 如何支持行列数不同的像素网格。

1. 引入视口相机的概念

为了便于处理编辑器内容的变换,这里引入 视口相机 (ViewCamera) 的概念。如下所示: - 红色区域是编辑器的最大区域,称之为 视口尺寸 (viewSize) ; - 蓝色区域是编辑器的实际的操作区,称之为 展示尺寸 (playSize)

image.png

可以休息一下 playSize 内的是现实世界的真实物体。现在将 viewSize 区域看做一个照相机。我们可以调节相机的位置、远近等控制真实物体在相机上的成像。这种图形的控制称为变换 ,一般通过 Matrix4 对象进行操作。
这里视口相机 ViewCamera 设计为 mixin,方便通过混入实现功能的独立。便于复用以及单一职责。此时,可以定义如下三个重要成员:

```dart mixin ViewCamera on ChangeNotifier { Size _viewSize = Size.zero; late Size _playSize; final Matrix4 _transformer = Matrix4.identity();

Size get viewSize => _viewSize; Size get playSize => _playSize; Matrix4 get transformer => _transformer; } ```


2. 两个尺寸的赋值

视口尺寸可以依赖外界设置。展示尺寸在 开始时 希望以适合大大小填充视口;网格长边留下 fixPadding 的边距;这样依赖视口尺寸,就可以算出网格适应边的大小;再根据网格尺寸,就可以算出每个网格的尺寸 pixSide

image.png

比如网格宽度大于长度时,左右两侧留下 fixPadding ,使其填充相机视口:

image.png

尺寸的计算逻辑如下所示,相机设置视口尺寸时,先检验和旧尺寸是否一致。如果未改变,直接返回不做处理。否则通过 _updatePlaySize 方法计算 playSize;然后通过 centerContent 方法通过变换操作将内容居中展示; onViewBoxChanged 是一个回调,来通知外界尺寸变化的时机:

```dart set viewSize(Size size) { if (size == _viewSize) return; Size oldSize = _viewSize; _viewSize = size; _updatePlaySize(size); centerContent(size, _playSize); scheduleMicrotask(() { onViewBoxChanged(oldSize, size); }); }

@protected void onViewBoxChanged(Size old, Size size) {} ```


playSize 的计算,需要依赖网格行列数,由于 ViewCamera 并不需要持有和维护该数据,可以通过 抽象方法 gridSize 交由混入它的类实现。计算过程也比较简单,根据 viewSize 计算出适合的像素边长 _pixSide ;乘以网格个行列数就可以的到 playSize :

```dart double _pixSide = 0; double get pixSide => _pixSide; (int, int) get gridSize; double fitPadding = 20;

void _updatePlaySize(Size viewSize) { double padding = fitPadding * 2; int row = gridSize.$1; int column = gridSize.$2; if (row > column) { _pixSide = (viewSize.width - padding) / row; } else { _pixSide = (viewSize.height - padding) / column; } _playSize = Size(gridSize.$1 * _pixSide, gridSize.$2 * _pixSide); } ```


3. 相机的变换操作

首先看一下平移操作。默认情况下,绘制会从画布的左上角开始。想要让其居中,可以通过平移变换。我们已经知道了 viewSizeplaySize 两个尺寸,就可以很容易地计算出偏移量。

image.png

这里希望当视口尺寸变化时,可以将网格区域适配呈现在中间,这就是 centerContent 的作用。它将变换矩阵重置为单位矩阵,并设置偏移量使视图居中。

dart void centerContent(Size viewBox, Size pixSize) { _transformer.setIdentity(); double dx = (viewBox.width - pixSize.width) / 2; double dy = (viewBox.height - pixSize.height) / 2; _transformer.translate(dx, dy); }

相机的移动通过 translation 方法处理,将 _transformer 乘以一个移动矩阵,并通知更新:

```dart void translation(double dx, double dy) { Matrix4 moveM = Matrix4.translationValues(dx / scale, dy / scale, 0); _transformer.multiply(moveM); notifyListeners(); }

double get scale => _transformer.getMaxScaleOnAxis(); ```


缩放操作最重要的是计算好缩放中心 center。缩放变换计算前,先通过移动将变换中心移到 center 点;计算完后再移回去。代码如下:

dart void setScale(double value, {Offset origin = Offset.zero}) { double dx = _transformer.getTranslation().x; double dy = _transformer.getTranslation().y; Offset center = (origin - Offset(dx, dy)) / scale; Matrix4 scaleM = Matrix4.diagonal3Values(value, value, 0); Matrix4 moveM = Matrix4.translationValues(center.dx, center.dy, 0); Matrix4 backM = Matrix4.translationValues(-center.dx, -center.dy, 0); _transformer.multiply(moveM); _transformer.multiply(scaleM); _transformer.multiply(backM); notifyListeners(); }


4. 视图层处理

视图层处理最重要的一点是,在绘制时使用相机中的 transformer 矩阵来对编辑区域的内容进行矩阵变换。我让 PixPaintLogic 混入了 ViewCamera,所以它就有视口相机的一切能力:

image.png

dart class PixPaintLogic with ChangeNotifier, ViewCamera { String activeLayerId = ''; final List<PaintLayer> _layers = [];


最后就是在拖拽移动和鼠标滚轮的事件监听和变换:

  • 通过 Listener#onPointerSignal 可以监听到鼠标的滚轮事件,其中触发缩放逻辑。
  • 通过 GestureDetector#onPanUpdate可以监听到鼠标的移动事件,其中触发平移逻辑。

image.png

在事件回调中,通过相机触发缩放和移动的方法即可:

```dart void onScale(PointerSignalEvent event) { if (event is PointerScrollEvent) { if (event.scrollDelta.dy < 0) { paintLogic.setScale(1.1, origin: event.localPosition); } else { paintLogic.setScale(0.9, origin: event.localPosition); } } }

void onMove(DragUpdateDetails details) { paintLogic.translation(details.delta.dx, details.delta.dy); } ```


5. 点击格点坐标校验

由于点击事件回调的触点时相对于视口左上角的偏移量。当视口进行缩放或者平移时,就需要进行相应的转换。将触点映射到变换后的坐标系中。下面画个移动时的示意图:
右图在移动之后,触点在点击第第二排第二个点时,触点的坐标还是以视口左上角为起点,我们需要将其原点视为 网格区域的左上角才能计算出正确的网格点位校验。实现很简单,就是将触点坐标减去偏移量即可,缩放同理:

image.png

我在相机中添加了 transformOffset 方法,将一个基于 视口左上角 的坐标,转换为基于 网格左上角 的坐标:

```dart Offset transformOffset(Offset src) { double dx = _transformer.getTranslation().x; double dy = _transformer.getTranslation().y; return (src - Offset(dx, dy)) / scale; }

(int x, int y) transformPoint(Offset src) { Offset offset = transformOffset(src); return (offset.dx ~/ pixSide, offset.dy ~/ pixSide); } ```

到这里,就是实现了自由地变换,不用受制于点击区域过小,可以更好地进行编辑。这也是像素编辑器最重要的一步。后续还会带来更多像素编辑器开发的文章,一起来见证这个小破项目的发展,敬请期待 ~

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

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

相关文章

【工具测评】ONLYOFFICE——你的下一款桌面编辑器

文章目录 前言一、安装1.1 跳转官网下载安装包1.2 安装步骤 二、功能介绍2.1 功能全面的 PDF 编辑器2.2 PDF 表单2.3 文本文档编辑器的更新2.4 电子表格编辑器的更新2.5 演示文稿编辑器有哪些更新2.6 所有编辑器中的改进内容2.7 从右至左显示 & 新的本地化选项2.8 可用性提…

基于Java超市库存管理系统设计和实现(源码+LW+调试文档+讲解等)

&#x1f497;博主介绍&#xff1a;✌全网粉丝10W,CSDN作者、博客专家、全栈领域优质创作者&#xff0c;博客之星、平台优质作者、专注于Java、小程序技术领域和毕业项目实战✌&#x1f497; &#x1f31f;文末获取源码数据库&#x1f31f;感兴趣的可以先收藏起来&#xff0c;还…

10.2 JavaEE——Spring MVC入门程序

要求在浏览器发起请求&#xff0c;由Spring MVC接收请求并响应&#xff0c;具体实现步骤如下。 一、创建项目 在IDEA中&#xff0c;创建一个名称为chapter10的Maven Web项目。 &#xff08;一&#xff09;手动设置webapp文件夹 1、单击IDEA工具栏中的File→“Project Structu…

学校校园考场电子钟,同步授时,助力考场公平公正-讯鹏科技

随着教育技术的不断发展&#xff0c;学校对于考场管理的需求也日益提高。传统的考场时钟往往存在时间误差、维护不便等问题&#xff0c;这在一定程度上影响了考试的公平性和公正性。为了解决这些问题&#xff0c;越来越多的学校开始引入考场电子钟&#xff0c;通过同步授时技术…

Nginx实现动静分离

目录 静态资源 动态资源 区别和应用场景 1. 准备环境 2. 配置代理 3. 静态资源主机配置 4. 动态资源主机配置 5. 访问静态和动态资源测试 测试1&#xff1a;访问静态资源 测试2&#xff1a;访问动态资源 动态资源和静态资源是在网络和Web开发中常用的两个概念&#…

「全新升级,性能更强大——ONLYOFFICE 桌面编辑器 8.1 深度评测」

文章目录 一、背景二、界面设计与用户体验三、主要新功能亮点3.1 高效协作处理3.2 共同编辑&#xff0c;毫无压力3.3 批注与提及3.4 追踪更改3.5 比较与合并3.6 管理版本历史 四、性能表现4.1 集成 AI 工具4.2 插件强化 五、用户反馈与使用案例 一、背景 Ascensio System SIA -…

48、基于深度学习的离群值输入向量(matlab)

1、基于深度学习的离群值输入向量原理及流程 基于深度学习的离群值检测的输入向量原理是通过神经网络模型对数据进行学习和表示&#xff0c;在该表示中探测异常样本。其流程大致如下&#xff1a; 数据预处理&#xff1a;将数据进行归一化处理&#xff0c;确保神经网络模型能够…

Java Scanner 类

Java Scanner 类 java.util.Scanner 是 Java5 的新特征&#xff0c;我们可以通过 Scanner 类来获取用户的输入。 下面是创建 Scanner 对象的基本语法&#xff1a; Scanner s new Scanner(System.in);接下来我们演示一个最简单的数据输入&#xff0c;并通过 Scanner 类的 nex…

如何评估LabVIEW需求中功能的必要性和可行性

评估LabVIEW需求中功能的必要性和可行性涉及多个方面的分析&#xff0c;包括需求的重要性、技术可行性、资源需求以及潜在风险。以下是一个详细的评估方法&#xff1a; ​ 一、功能必要性评估 需求来源和目的&#xff1a; 来源&#xff1a;需求来自哪里&#xff1f;是客户、市…

Windows系统下安装RabbitMQ详细步骤

声明&#xff1a;原文参考链接出自&#xff1a; 如何在Windows系统下安装RabbitMQ_rabbitmq windows安装-CSDN博客 https://zhuanlan.zhihu.com/p/693160757 一、RabbitMQ安装软件资源准备 因为RabbitMQ是Erlang语言开发的&#xff0c;因此安装Erlang环境在进行安装RbbitMQ的…

贺尔碧格流量阀比例放大器PSR2BE10P25、PSR2BE10P30、PSR2BE10P25

PSR2BE04N06、PSR2BE04P10、PSR2BE04P06、PSR2BE04N10、PSR2BE10N12、PSR2BE10P25、PSR2BE10P30、PSR2BE10P25、PSR3BE10N25、PSR3BE10P30、PSR3BE10P12贺尔碧格HOERBIGER液压比例流量阀由比例电磁铁和流量阀组合而成&#xff0c;利用输入的电信号来改变节流阀的开度&#xff0…

深入解析 iOS 应用启动过程:main() 函数前的四大步骤

深入解析 iOS 应用启动过程&#xff1a;main() 函数前的四大步骤 背景描述&#xff1a;使用 Objective-C 开发的 iOS 或者 MacOS 应用 在开发 iOS 应用时&#xff0c;我们通常会关注 main() 函数及其之后的执行逻辑&#xff0c;但在 main() 函数之前&#xff0c;系统已经为我们…

[Django学习]前端+后端两种方式处理图片流数据

方式1&#xff1a;数据库存放图片地址,图片存放在Django项目文件中 1.首先&#xff0c;我们现在models.py文件中定义模型来存放该图片数据,前端传来的数据都会存放在Django项目文件里的images文件夹下 from django.db import modelsclass Image(models.Model):title models.C…

SiLM5350系列SiLM5350SABCA-DG 10A30V提供分离输出 单通道隔离栅极驱动器

SiLM5350系列SiLM5350SABCA-DG是具体有10A峰值输出电流能力&#xff0c;单通道隔离式栅极驱动器。SiLM5350系列SiLM5350SABCA-DG提供分离输出&#xff0c;可分别控制上升和下降时间。驱动电源电压为4V至30V。3V至18V的宽输入VDDI范围使驱动器适合与模拟和数字控制器接口。所有电…

c++网络通信

TCP/IP协议 OSI参考模型采用分层划分原则&#xff0c;将网络中的数据传输划分为7层&#xff0c;其中&#xff0c;物理层居于最下层&#xff0c;是最基础、核心的网络硬件层&#xff1b;应用层居于最上层&#xff0c;负责应用资源的管理。每一层使用下层的服务&#xff0c;并向…

Python爬取中国福彩网彩票数据并以图表形式显示

网页分析 首先打开中国福彩网&#xff0c;点击双色球&#xff0c;选择往期开奖栏目 进入栏目后&#xff0c;选定往期的奖金数目作为我们想要爬取的目标内容 明确目标后&#xff0c;开始寻找数据所在的位置 鼠标右击页面&#xff0c;打开网页源代码&#xff0c;在源代码中搜索…

<Rust><iced>在iced中显示gif动态图片的一种方法

前言 本文是在rust的GUI库iced中在窗口显示动态图片GIF格式图片的一种方法。 环境配置 系统&#xff1a;window 平台&#xff1a;visual studio code 语言&#xff1a;rust 库&#xff1a;iced、image 概述 在iced中&#xff0c;提供了image部件&#xff0c;从理论上说&…

手机删除照片后还可以恢复吗?5个步骤,教你掌握正确方法

手机里的照片是我们记录生活、珍藏回忆的宝库&#xff0c;但有时候我们可能会不小心删除照片&#xff0c;或者因为各种原因需要恢复已经删除的照片。别担心&#xff0c;这篇文章将为你提供关于手机照片恢复的全面指南&#xff0c;揭开手机照片的恢复之谜&#xff0c;重新拥有那…

前端:Element UI 与 Vuetify 的选择

vuetify优势 1、多端适配&#xff0c;Vuetify完全按照Material设计规范进行开发&#xff0c;每一个组件都经过精心设计&#xff0c;具有模块化、响应式和优秀的性能。 使用独特和动态的 布局 自定义您的应用程序&#xff0c;并使用 SASS 变量 自定义您的组件的样式。只需要做下…

AI入门系列:工具篇之ChatGPT的优秀的国内替代品

文章目录 一&#xff0c;智谱清言(ChatGLM)1&#xff0c;智谱清言简介2&#xff0c;[智谱清言地址&#xff0c;点我开始用吧](https://chatglm.cn/) 二&#xff0c;Kimi智能助手1&#xff0c;Kimi简介2&#xff0c;[Kimi地址&#xff0c;点我开始用吧](https://kimi.moonshot.c…