likes
comments
collection
share

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

作者站长头像
站长
· 阅读数 10

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


0.本文目的

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

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

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

1. 引入视口相机的概念

为了便于处理编辑器内容的变换,这里引入 视口相机 (ViewCamera) 的概念。如下所示:

  • 红色区域是编辑器的最大区域,称之为 视口尺寸 (viewSize)
  • 蓝色区域是编辑器的实际的操作区,称之为 展示尺寸 (playSize)

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

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

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

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

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

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

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

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 :

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 两个尺寸,就可以很容易地计算出偏移量。

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

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

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 乘以一个移动矩阵,并通知更新:

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 点;计算完后再移回去。代码如下:

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,所以它就有视口相机的一切能力:

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

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

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

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

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

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

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. 点击格点坐标校验

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

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

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

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

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

转载自:https://juejin.cn/post/7383363236680613923
评论
请登录