前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Flutter 像素编辑器#05 | 缩放与平移

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

作者头像
张风捷特烈
发布2024-06-25 15:07:41
570
发布2024-06-25 15:07:41
举报

0.本文目的

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

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

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

1. 引入视口相机的概念

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

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

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

代码语言:javascript
复制
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

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

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

代码语言:javascript
复制
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 :

代码语言:javascript
复制
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 两个尺寸,就可以很容易地计算出偏移量。

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

代码语言:javascript
复制
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 乘以一个移动矩阵,并通知更新:

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

代码语言:javascript
复制
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,所以它就有视口相机的一切能力:

代码语言:javascript
复制
class PixPaintLogic with ChangeNotifier, ViewCamera {
  String activeLayerId = '';
  final List<PaintLayer> _layers = [];

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

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

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

代码语言:javascript
复制
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. 点击格点坐标校验

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

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

代码语言:javascript
复制
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);
}

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

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2024-06-25,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1. 引入视口相机的概念
  • 2. 两个尺寸的赋值
  • 3. 相机的变换操作
  • 4. 视图层处理
  • 5. 点击格点坐标校验
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档