图形编辑器开发:以光标位置缩放画布

news2024/11/15 12:32:39

大家好,我是前端西瓜哥。

画布缩放是图形设计工具中很重要的基础能力。

通过它,我们可以像举着一台摄影机,在图形所在的世界到处游逛,透过镜头,可以只看自己想看的图形;可以拉近摄影机,看到图形的细节;也可以拉远摄影机,总览多个图形之间的关系。

ok,那么我们看看如何实现缩放画布功能。

文中的动图演示来自我正在开发的图形设计工具:

https://github.com/F-star/suika

线上体验:

https://blog.fstars.wang/app/suika/

场景坐标系和视图坐标系

场景坐标系 就是图形所在的二维平面世界,所使用的坐标系。单位是像素(px)。坐标系的原点在画布(canvas 元素)的左上角,x 轴向右,y 轴向下。

图形会被绘制到这个平面,理论上它的范围是可以 无限延展 的。(不过实际上我们会给一个上限,但这个值也非常大。无限大的话没有意义,且浮点数是取值范围的)

然而显示器的宽高是有限的,只能看一个矩形范围内的内容。

所以我们需要引入一个 “摄影机”:视图坐标系,只看部分的区域。

其实就是将原来真实的图形的坐标做一个线性计算。

首先是将特定区域 移动 到视口中,就像摄影机从原点移动我们想要观察的某个物体上。不过实际上是物体所在的平面做了一个方向的移动。

在这里插入图片描述

然后再做一个缩放,就像摄影机拉近或远离与目标物体距离,效果是物体在镜头下变大或变小。

在这里插入图片描述

转换就两步,移动然后缩放

视图矩阵转换

场景坐标系到视图坐标系的转换,我们通过 视图矩阵 相乘来实现。

事实上,任意两个坐标系下坐标的转换,都可以通过一个矩阵乘法来实现。

关于矩阵和矩阵乘法,可以看我的这篇文章

《计算机图形学:变换矩阵》

首先是将坐标进行位移,x 方向位移 -viewport.x,y 方向位移 -viewport.y。这里是负数,虽然我们想要移动 “摄影机”这是因为移动的是画布

<平移矩阵> * 坐标

然后再缩放(缩放值我们会用 zoom 表示):

<缩放矩阵> * 平移后的坐标

所有过程写在一起,就是:

<缩放矩阵> * <平移矩阵> * 坐标

矩阵乘法符合结合律,所以我们的视图矩阵为:

  <视图矩阵>
= <缩放矩阵> * <平移矩阵>

矩阵表示为:

计算结果为:

对应的 Canvas 2D 代码:

ctx.scale(zoom, zoom);
ctx.translate(-viewport.x, -viewport.y);

写成一个方法:

// 场景坐标转视图坐标
function sceneCoordsToViewport(x, y, zoom, scrollX, scrollY) {
  return {
		x: (x - scrollX) * zoom,
		y: (y - scrollY) * zoom
  };
}

至于反过来,场景坐标系转视图坐标,计算它的逆矩阵即可:

以光标为中心缩放

在这里插入图片描述

首先我们来认清本质,所谓以光标为中心缩放,不变的是什么?

光标所在点在视图坐标系距离视口左上角的相对位置,保持不变

我们要做的事是,在 zoom 变化后,调整 viewport.x 和 viewport.y 的值,让光标在视图坐标系上相对视口左上角距离不变。

这里得补充一个知识点。就是两个坐标系中距离的转换:

  1. 场景转视图,距离转换为 dist * zoom
  2. 视图转场景,距离的转换是 dist / zoom。因为视口看到的图形都是缩放(乘以 zoom)后的结果,所以反过来就要除回去。

实现思路是:

  1. 记录好缩放前,光标所在位置的场景坐标;
  2. 计算 (cx, cy) 在旧缩放比(zoom)的场景坐标。
  3. 计算 cx 在新的缩放比(zoom)下,(cx / zoom, cy / zoom)
  4. 然后二者相减,即可得到新的适口左上角坐标。

代码实现为:

/**
 * 以某点为中心,进行画布缩放
 * @param {number} zoom 新的缩放比
 * @param {number} cx 缩放中心(使用视图坐标)
 * @param {number} cy
 */
const setZoomAndUpdateViewport = (zoom, cx, cy) => {
  const prevZoom = this.zoom;
  this.zoom = zoom;

  // 计算缩放中点的场景坐标
  const { x: sceneCX, y: sceneCY } = viewportCoordsToScene(
    cx,
    cy,
    prevZoom,
    this.viewport.x,
    this.viewport.y,
  );

  // 核心代码
  const newViewportX = sceneCX - cx / zoom;
  const newViewportY = sceneCY - cy / zoom;

  this.viewport.x = newViewportX;
  this.viewport.y = newViewportY;

  this.renderScene();
};

以画布为中心进行缩放

如果缩放时光标不在画布上,比如通过手动输入缩放值时,会 以画布的中心位置进行缩放

实现同上,只是 cx 和 cy 改成传入视口(即画布)的宽高除以 2:(viewport.width / 2), (viewport.height / 2)

在这里插入图片描述

结尾

要实现画布缩放,重点是理解场景坐标和视图坐标之间的关系。

场景坐标转视图坐标,首先需要将画布进行移动,让场景坐标的原点和视图坐标的原点对上(场景坐标移动 -viewport.x 和 -viewport.x),然后再进行缩放(乘以 zoom)。

我是前端西瓜哥,欢迎关注我,学习更多图形编辑器知识。

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

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

相关文章

SpringBoot 如何使用 @ControllerAdvice 注解进行全局异常处理

SpringBoot 如何使用 ControllerAdvice 注解进行全局异常处理 在 Web 开发中&#xff0c;异常处理是非常重要的一环。在 SpringBoot 框架中&#xff0c;我们通常使用 ExceptionHandler 注解来处理 Controller 层的异常。但是&#xff0c;如果想要处理全局异常&#xff0c;我们…

【MySql】MySql事务常见操作

文章目录 准备工作事务常见操作方式总结 准备工作 将mysql的默认隔离级别设置成读未提交 set global transaction isolation level read uncommitted;注意&#xff1a;设置完毕之后&#xff0c;需要重启终端&#xff0c;进行查看 select tx_isolation 创建测试表 mysql>…

HTML5 的新特性

html基础知识html基础知识_上半场结束&#xff0c;中场已休息&#xff0c;下半场ing的博客-CSDN博客html5的新特性HTML5 的新特性_上半场结束&#xff0c;中场已休息&#xff0c;下半场ing的博客-CSDN博客 目录 1.0 HTML5 的新特性 1.1 HTML5 新增的语义化标签 1.2 HTML5 新增的…

LabVIEW何得知是谁在连接远程前面板

LabVIEW何得知是谁在连接远程前面板 想要知道连接远程前面板的用户的身份。如何来得知用户的身份和他们连接远程前面板的时间&#xff1f; 解答: 可以使用Remote Panel: Connections To Clients属性或者Remote Panel Client Connections方法来得知连接远程面板用户的身份。Re…

TypeScript ~ TS 掌握编译文件配置项 ⑤

作者 : SYFStrive 博客首页 : HomePage &#x1f4dc;&#xff1a; TypeScript ~ TS &#x1f4cc;&#xff1a;个人社区&#xff08;欢迎大佬们加入&#xff09; &#x1f449;&#xff1a;社区链接&#x1f517; &#x1f4cc;&#xff1a;觉得文章不错可以点点关注 &…

【2023,学点儿新Java-20】流程控制语句关键字及其介绍:while、do、break、continue、return

前情回顾&#xff1a; 【2023&#xff0c;学点儿新Java-19】Java特殊空类型关键字 | Java流程控制语句关键字 | switch-case 选择结构的执行过程&注意点 | 详解&#xff1a;for循环的普通和增强版本【2023&#xff0c;学点儿新Java-18】Java关键字汇总说明 |附&#xff1a…

chatgpt赋能python:Python新手常见的报错提示及解决方法

Python新手常见的报错提示及解决方法 Python是一种非常流行的编程语言。对于新手来说&#xff0c;Python在学习过程中可能会遇到许多报错提示&#xff0c;这些提示可能会让人感到很困惑。本文将介绍Python新手常见的报错提示&#xff0c;并提供解决方法。 IndentationError: …

ninja的简单使用

文章目录 Ninja安装windows环境Linux环境 入门使用与CMake一起使用 Ninja安装 windows环境 问题的解决通常有多种方法。按照结果的好坏程度&#xff0c;可以将解决方法简单的划分为&#xff0c;上中下三个层次&#xff0c;见:为什么谋士总喜欢提上中下三策&#xff1f; 在w…

SpringBoot 如何使用 @RestControllerAdvice 注解进行 RESTful 异常处理

SpringBoot 如何使用 RestControllerAdvice 注解进行 RESTful 异常处理 在 SpringBoot 应用程序中&#xff0c;RESTful 异常处理是一个非常重要的话题。当 RESTful API 出现异常时&#xff0c;我们需要对异常进行处理&#xff0c;以保证 API 的稳定性和可靠性。SpringBoot 提供…

【SpringBoot】SpringBoot的发展沿革,相关介绍,特点,重要策略以及安装步骤讲解

作者简介&#xff1a; 辭七七&#xff0c;目前大一&#xff0c;正在学习C/C&#xff0c;Java&#xff0c;Python等 作者主页&#xff1a; 七七的个人主页 文章收录专栏&#xff1a; 七七的闲谈 欢迎大家点赞 &#x1f44d; 收藏 ⭐ 加关注哦&#xff01;&#x1f496;&#x1f…

Java·Map和Set

文章目录 &#x1f3ef;1. 搜索&#x1f3ef;1.1 概念及场景1.2 模型 &#x1f3f0;2. Map 的使用&#x1f3f0;2.1 关于Map的说明2.2 关于Map.Entry的说明2.3 Map 的常用方法说明2.4 TreeMap的使用案例2.5HashMap源码分析 ⛺️3. Set 的说明⛺️3.1 常见方法说明 &#x1f3ed…

设计模式—访问者模式

需求&#xff1a;店铺采购了一批水果&#xff08;苹果及橘子&#xff09;&#xff0c;现在市场监督局来店里检查过期的水果。 public class Fruit {private String name;private Date pickDate;public Fruit(String name, Date pickDate) {this.name name;this.pickDate pic…

【Unity之IMGUI】—位置信息类和控件基类的封装

&#x1f468;‍&#x1f4bb;个人主页&#xff1a;元宇宙-秩沅 &#x1f468;‍&#x1f4bb; hallo 欢迎 点赞&#x1f44d; 收藏⭐ 留言&#x1f4dd; 加关注✅! &#x1f468;‍&#x1f4bb; 本文由 秩沅 原创 &#x1f468;‍&#x1f4bb; 收录于专栏&#xff1a; ⭐…

出现线程死锁缺陷一般有那些原因?该怎么解决?

&#x1f482; 个人网站:【海拥】【游戏大全】【神级源码资源网】&#x1f91f; 前端学习课程&#xff1a;&#x1f449;【28个案例趣学前端】【400个JS面试题】&#x1f485; 寻找学习交流、摸鱼划水的小伙伴&#xff0c;请点击【摸鱼学习交流群】 目录 前言什么是线程死锁线程…

【Python】文件操作 ⑤ ( 文件操作 | 以只读模式向已有文件写入数据 | 以追加模式向已有文件写入数据 | 以追加模式打开一个不存在的文件 )

文章目录 一、向文件写出数据1、以只读模式向已有文件写入数据2、以追加模式向已有文件写入数据3、以追加模式打开一个不存在的文件 一、向文件写出数据 1、以只读模式向已有文件写入数据 使用 write 函数向已有文件写入数据 , 会清空该文件中的数据 , 代码展示如下 : file1.t…

当mysql遇上PHP

一.利用PHP连接mySQL数据库 这要从一个故事说起。 某一天&#xff0c;一位名叫MySQL的农夫的一把斧子&#xff08;数据库操作&#xff09;掉进了一条名为PHP的河里&#xff0c;这时候&#xff0c;一位好心的河神出现了 PHP河的河神问他。。。。 下面&#xff0c;咱们还是说正经…

Kubernetes API Server源码学习(三):KubeAPIServer、APIExtensionsServer、AggregatorServer

本文基于Kubernetes v1.22.4版本进行源码学习 9、KubeAPIServer GenericAPIServer提供了一些通用的功能&#xff0c;其他的Server基于GenericAPIServer进行拓展&#xff0c;代码量就会减少许多 KubeAPIServer负责处理Kubernetes内建资源的REST请求&#xff0c;比如Pod、Deploy…

C/C++系列系统学习目录

友情链接&#xff1a;专栏地址 文章目录 一、C语言篇二、C篇 编程规范&#xff1a;C/C语言编程规范 一、C语言篇 章节内容1.初识C语言【C语言篇】初识C语言2.C语言最基础入门【C语言篇】C语言最基础入门3.C语言的输入输出相关知识【C语言篇】C语言的输入/输出相关知识4.C语言…

【Pytest实战】Pytest+Allure+Jenkins自动化测试框架搭建

&#x1f604;作者简介&#xff1a; 小曾同学.com,一个致力于测试开发的博主⛽️&#xff0c;主要职责&#xff1a;测试开发、CI/CD 如果文章知识点有错误的地方&#xff0c;还请大家指正&#xff0c;让我们一起学习&#xff0c;一起进步。&#x1f60a; 座右铭&#xff1a;不想…

从零开始理解Linux中断架构(5)--EL跃迁与Linux用户/内核态

ARM64系统Reset时,PE进入最高的异常级别运行状态 1)Reset后最高异常级别可以选用任何一种运行状态 2)cold reset由输入信号配置,warm reset由RMR_ELx.AA64配置 1)内核态EL1迁移到EL0t 上一节我们提到需要仔细理解的图吗?eret这条特殊的异常返回指令。 我们期望…