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.mzph.cn/diannao/34844.shtml

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

相关文章

Docker - Oracle Database 23ai Free

博文目录 文章目录 说明命令NavicatSYSTEMPDBADMIN 扩展公共用户本地用户 说明 Oracle 官方镜像仓库 Database 23ai Free | Oracle Docker 官方没有提供 Oracle Database 相关镜像, 但是 Oracle 官方镜像仓库有提供, 打开上面的链接, 选择 Database, 选择合适的版本, 如 enter…

Elasticsearch:使用 Llamaindex 的 RAG 与 Elastic 和 Llama3

这篇文章是对之前的文章 “使用 Llama 3 开源和 Elastic 构建 RAG” 的一个补充。我们可以在本地部署 Elasticsearch&#xff0c;并进行展示。我们将一步一步地来进行配置并展示。你还可以参考我之前的另外一篇文章 “Elasticsearch&#xff1a;使用在本地计算机上运行的 LLM 以…

Android frida 实战: 分析全民K歌的判断逻辑

本篇进入 Android frida 实战&#xff0c;旨在分析学习全民K歌这个 app 演唱页面的判断逻辑。 版本&#xff1a;8.22.38.278 此 app 为腾讯推出的面向国内的社交娱乐类应用软件&#xff0c;主要功能是提供用户唱歌、录制和分享自己演唱的歌曲。当非 vip 用户演唱某 vip 歌曲等功…

三元前驱体废水回收镍钴工艺:环保与经济效益的双重胜利

在全球新能源产业迅猛发展的背景下&#xff0c;锂离子电池作为绿色能源的核心组件&#xff0c;其需求量激增&#xff0c;带动了上游材料市场&#xff0c;尤其是三元前驱体材料的蓬勃发展。然而&#xff0c;伴随着行业的快速扩张&#xff0c;三元前驱体生产过程中产生的含镍钴废…

Qt开发 | Qmake与CMake | Qt窗口基类 | VS Qt项目与QtCreator项目相互转化 | Qt架构 | Qt学习方法

文章目录 一、Qmake与CMake介绍1.Qmake2.CMake3.使用qmake还是cmake&#xff1f; 二、Qt3个窗口基类的区别三、vs qt与QtCreator项目相互转化方法1.QtCreator项目转VS Qt2.VS Qt项目转QtCreator项目 四、Qt架构介绍与学习方法详解 一、Qmake与CMake介绍 Qmake和CMake都是构建系…

干货分享 | TSMaster 中不同总线报文消息过滤的操作方式

TSMaster软件平台支持对不同总线&#xff08;CAN、LIN、FlexRay&#xff09;报文和信号的过滤&#xff0c;包括全局接收过滤、数据流过滤、窗口过滤、字符串过滤、可编程过滤&#xff0c;针对不同的总线信号过滤器的使用方法基本相同。今天重点和大家分享一下关于TSMaster中报文…

全国首场以AI数字内容风控为主题的大会正式官宣,首批演讲嘉宾和议题揭晓!

曾经我们感叹的“AI迎来了iPhone时刻”&#xff0c;如今已变成“iPhone迎来了AI时刻”。前段时间&#xff0c;苹果全球开发者大会的召开&#xff0c;以及闻声而起的资本市场&#xff0c;无一不再次佐证了AI的无穷想象。 从OpenAI直播演示GPT-4o和谷歌的I/O开发者大会2024&…

Unity踩坑记录

1. 如果同时在父物体和子物体上挂载BoxCollider&#xff0c;那么当使用&#xff1a; private void OnTriggerEnter2D(Collider2D collision){if (collision.CompareTag("CardGroup")){_intersectCardGroups.Add(collision.GetComponent<CardGroup>());}} 来判…

【linux学习十七】文件服务管理

一、FTP FTP server:FTP(File Transfer Protocol,文件传输协议 )是 TCP/IP 协议组中的协议之一 软件包&#xff1a;vsftpd/安装 yum -y install vsftpd//准备文件 touch /var/ftp/abc.txt //注释:FTP服务器的主目录:“/var/ftp/”&#xff0c;是FTP程序分享内容的本机目录…

数据库 复习题

有一个关系模式&#xff1a;工程关系&#xff08;工程号&#xff0c;工程名称&#xff0c;职工号&#xff0c;姓名&#xff0c;聘期&#xff0c;职务&#xff0c;小时工资率&#xff0c;工时&#xff09;&#xff0c;公司按照工时和小时工资率支付工资&#xff0c;小时工资率由…

【大数据】—二手车用户数据可视化分析案例

项目背景 在当今的大数据时代&#xff0c;数据可视化扮演着至关重要的角色。随着信息的爆炸式增长&#xff0c;我们面临着前所未有的数据挑战。这些数据可能来自社交媒体、商业交易、科学研究、医疗记录等各个领域&#xff0c;它们庞大而复杂&#xff0c;难以通过传统的数据处…

MySQL数据库(二):数据库基本操作

MySQL是一种流行的关系型数据库管理系统&#xff0c;广泛用于Web应用和各种数据存储需求。通过本次介绍&#xff0c;您将学习如何进行MySQL数据库的基本操作&#xff0c;包括创建数据库和表、插入和查询数据、更新和删除记录。这些基础知识将为您打下坚实的数据库操作基础。 目…

2023国家最高科学技术奖薛其坤院士:科学家的幸福感来自于哪里

内容来源&#xff1a;量子前哨&#xff08;ID&#xff1a;Qforepost&#xff09; 文丨浪味仙 排版丨沛贤 深度好文&#xff1a;2000字丨8分钟阅读 6 月 24 日&#xff0c;2023 年度国家最高科学技术奖在京揭晓&#xff0c;薛其坤院士荣获中国科技界崇高荣誉&#xff0c;这不…

【软件下载】Camtasia Studio 2024详细安装教程视频

习惯上来说Camtasia Studio是一款简单易用的高清录屏和视频编辑软件&#xff0c;拥有录制屏幕和配音、视频的剪辑和过场动画片、添加说明字幕和水印、制作视频封面和菜单、视频压缩和播放。不得不说Camtasia是一款屏幕录制和视频剪辑软件&#xff0c;教授课程&#xff0c;培训他…

字节跳动联手博通:5nm AI芯片诞生了?

字节跳动联手博通&#xff1a;5nm AI芯片诞生了&#xff1f; 前言 就在6月24日&#xff0c;字节跳动正在与美国博通合作开发一款5纳米工艺的专用集成电路(ASIC) AI处理器。这款芯片旨在降低采购成本并确保高端AI芯片的稳定供应。 根据报道&#xff0c;尽管芯片设计工作进展顺利…

力扣SQL50 即时食物配送 II min函数 嵌套查询

Problem: 1174. 即时食物配送 II &#x1f468;‍&#x1f3eb; 参考题解 Code -- 计算立即配送的订单百分比 select round (-- 计算订单日期与客户偏好配送日期相同的订单数量sum(case when order_date customer_pref_delivery_date then 1 else 0 end) * 100 /-- 计算总订…

【linux学习十六】网络管理

网络管理器(NetworkManager)是一个动态网络的控制器与配置系统&#xff0c;它用于当网络设备可用时保持设备和连接开启并激活 默认情况下&#xff0c;CentOS/RHEL7已安装网络管理器&#xff0c;并处于启用状态。 认识网卡 ens32 ens33 ens34 ens35 一.ip相关 查询网络状态 sy…

2005年下半年软件设计师【下午题】试题及答案

文章目录 2005年下半年软件设计师下午题--试题2005年下半年软件设计师下午题--答案 2005年下半年软件设计师下午题–试题 2005年下半年软件设计师下午题–答案

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

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

JVM-类加载机制

一、基础概念 当我们用java命令运行某个类的main函数启动程序时&#xff0c;首先需要通过类加载器把该类加载到JVM。 其主要流程如下&#xff1a; 1.什么是类加载 那么什么是类加载&#xff1f; Java的类加载&#xff0c;就是把字节码格式“.class”文件加载到JVM的方法区…