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


theme: cyanosis

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


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

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

  • 5
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值