0


从零手撕一个网页版图形编辑器之坐标变换(3)


本编辑器(土豆猫图形编辑器)社区版代码已开源,开源库地址:https://gitee.com/longhan13/lgxmap_community.git

本文暂时中断前面章节的代码框架讲解,先讲解一下本编辑器所使用的坐标变换方法及涉及的相关代码,是本编辑器基础的基础。本编辑器所使用的坐标系为右手坐标系,既X正向水平朝屏幕右边,Y轴正向垂直朝屏幕上方,Z轴正向垂直于屏幕指向屏幕外面。

在这里讲解坐标变换的时候没有像常见的计算机图形学书籍那样,一来就使用矩阵,个人认为这种讲法对于数学已经忘记差不多的人来说更难以理解,而是先用一种容易理解的方法来讲解屏幕坐标到世界坐标相互变换。鼠标绘图、鼠标拾取图形使用的是屏幕坐标到世界坐标的变换;图形在屏幕上显示,则使用的是世界坐标到屏幕坐标的变换。

    下面所讲的屏幕坐标都是以以H5 canvas画布左上角为原点,X轴朝东,Y轴朝南的坐标系,其X范围为(0,画布宽度s_width),Y范围为(0,画布高度s_height)

    当然本编辑器内部还是会使用到矩阵工具,用于图形任意状态下(如进行了旋转、缩放操作后)都可以进行鼠标拖拽进行拉伸缩放。

下面正式开始。

首先先看一张图:

图1 坐标变换

图中渐变填充的三角形表示世界坐标系下的一个物体。

1)、黑色矩形框分别表示世界坐标系范围,(w_orgX,w_orgY)为世界坐标系原点,w_width为世界范围宽度,w_height为时间范围高度;

2)、蓝色矩形表示画布屏幕坐标系范围,左上角(s_orgX,s_orgY)为原点,恒等于(0,0),s_width和s_height分别表示画布的屏幕尺寸宽度和高度

3)、紫色矩形框表示当前视口范围(既在此区域内的图形对象才会显示并且等比例投影到画布),视口范围下的参数都是位于屏幕坐标系下,(v_left,v_bottom)表示视口区域左下角,(v_right,v_top)表示视口区域右上角。

所谓的世界坐标到屏幕坐标的变换,就是要把视口范围内的图形投影到屏幕进行显示,世界坐标范围(可自定义,默认跟画布范围一样,只不过世界原点变成了屏幕右下角,Y轴朝上)在编辑器初始化就固定下来了,视口范围在随着我们缩放画布、拖动画布而变化,我们看到图形放大显示了实际就是视口范围变小了;拖动画布,实际就是视口范围的原点(v_left,v_bottom)变化了。

一、初始状态(图形不缩放、不拖动、视口范围完全等于世界范围)

图2 初始态坐标变换实例

我们设定初始状态下画布的左下角跟世界坐标系原点重合(粗黑色框表示世界坐标系范围,细一点的紫色矩形框表示初始状态下视口区域),整个世界坐标范围正好完全在画布内显示,相当于我们把世界范围想象成一块可等比拉伸的橡皮条(长w_width,宽w_height),把它左下角(w_orgX,w_orgY)用于一个钉子钉在屏幕的左下角(0,s_height),这时候我们就得到两个压缩系数:

  projSX = w_width/s_width-------世界坐标X方向到屏幕X方向的压缩率

  projSY = w_height/s_height-------世界坐标Y方向到屏幕Y方向的压缩率

因为这两个压缩比例可能是不一样的,为了让我们的图形显示的时候不变形(如把世界坐标系下的一个正方形显示成了一个长方形),所以我们取大者作为整体投影缩放系数:

projectScale = max(projSX,projSY)------世界坐标到屏幕坐标的投影缩放系数

这个值表示一个屏幕坐标单位对应于世界坐标的多少个单位。为了便于理解,我们把画布尺寸和世界坐标取成好理解的实际值来讲解,我们假设w_width = 1000,w_height=800,画布尺寸为(100x80),那么projectScale=10,假设我们在世界坐标系下有一根直线(0,0)到(0,100),那么这根直线在世界坐标系下长度为100,投影到画布上进行显示,它的长度就只有10个屏幕单位了。

有些朋友可能会问, projectScale = max(projSX,projSY)为什么取大者而不是取小者呢?因为世界坐标到屏幕坐标是除的关系,这个系数越小,那得到的屏幕尺寸就会越大,当projSX和projSY 不相等时,就会有部分在视口范围内,但是屏幕上却显示不出来的图形,例如画布尺寸只有100x80,但是换算得到一个坐标点为(200,160),肯定就显示不出来了,而取大者就不会存在这个问题。

为了说明换算过程,我们看上图,假设WP1(500,400)为世界坐标上的一个点,那么投影到画布显示时它的屏幕坐标就是SP1(40,45),计算过程如下:

因为这时候视口原点跟世界原点重合,既(v_left,v_bottom)= (m_orgX,m_orgY),我们设世界坐标WP(wx,wy),投影后的屏幕坐标为(sx,sy),则:

sx = (wx - v_left)/projectScale = (wx - w_orgX)/projectScale = (500 - 100)/10 = 40

sy = s_height - (wy - v_bottom)/projectScale = s_height - (wy - w_orgY)/projectScale = 80 - (400- 50)/10 = 45------这里计算Y时,为什么要由画布高度s_height来减呢?通俗来讲,那是因为通过 (wy - v_bottom)/projectScale计算后得到值是相对于画布左下角点(0,s_height),既这个值越小它的Y值就越接近于画布的底部,越大就越接近画布顶部。如果用计算机图形学的矩阵来讲就是进行了投影变换,变换矩阵是:

图3 投影变换矩阵

二、拖动状态(图形不缩放,进行拖动,视口原点发生变动)

图4 无缩放拖动状态下坐标换算实例

以上图为例进行说明,我们假设视口区域发生了移动,视口原点(v_left,v_bottom)移动到了(200,150)。假设WP1(500,400)为世界坐标上的一个点,那么投影到画布显示时它的屏幕坐标就是SP1(30,55),计算过程如下:

设世界坐标WP(wx,wy),投影后的屏幕坐标为(sx,sy),则:

sx = (wx - v_left)/projectScale = (500 - 200)/10 = 30

sy = s_height - (wy - v_bottom)/projectScale = 80 - (400- 150)/10 = 55

二、复杂状态(图形进行了缩放,也进行了拖动,既视口原点发生变动,视口尺寸也发生变化)

图5 复杂显示状态下坐标换算实例

所谓缩放,就是视口区域与世界区域不一致,例如变大了或者变小了,举个例子来说,我们把窗户当作画布,窗户外面有座山,山上有一座庙,苗里还有个和尚在敲木鱼,当我要通过这个窗户完整看到整座山的时候,视口区域就是整座山的包围盒矩形,这时候我看到的庙肯定是很小的,几乎是一个点,庙里的和尚和木鱼也变成了一点,当我要想看清楚庙里甚至和尚和木鱼的时,我就得把视口区域变小,变成跟庙得包围盒矩形一样大,相当于有个超人抱着我的窗户,飞到庙的前面,从窗户看庙,刚好把庙包围住,这时候从视觉上看图形肯定就变大了。

    从这里开始,我们引入了另外一个缩放系数,图形全局缩放系数(后面还会涉及到单个图形如模具图形、矩形、椭圆等单个图形得局部缩放系数),我们命名此系数为:scaling,这个系数可以通过如下公式计算得到:

scaling_x = w_width/(v_right - v_left)

scaling_y = w_height/(v_top - v_bottom)

取小者,得scaling = min(scaling_x,scaling_y)。取小者得原因跟前面计算投影缩放系数一样,都是为了让视口内的图形都在画布内显示,但因为本系数用于世界坐标到屏幕坐标的换算中是乘的发关系,所以取小者。

实际应用中,我们很少会用这种方法来计算scaling(框选一个矩形区域然后放大显示的时候用到),而是对当前值乘以一个系数进行放大或缩小,例如滚轮缩放的时候,前滚放大scaling *=1.2,后滚缩小scaling /=1.2(1.2这个系数是随便定义的,实际编码中可自定义设置,想缩放速度快这个值就取大些)。

下面给出我们考虑了scaling后的完整的坐标换算公式,假设世界坐标点(wx,wy):

let scale = scaling / projectScale;

let sx = (wx - v_left) * scale;

let sy = s_height -  (wy - v_bottom) * scale;

let screenPt = {sx, sy };

screenPt就是世界坐标点(wx,wy)在画布上的投影坐标。

我们以上图中的实际点wp1(500,400)来说明上面公式的应用。

首先计算scaling,

scaling_x = w_width/(v_right - v_left) = 1000/(700 - 200) = 2

scaling_y = w_height/(v_top - v_bottom) = 800/(550 - 350) = 2

scaling = min(scaling_x ,scaling_y) = 2

let scale = scaling / projectScale = 2/10 = 0.2;

let sx = (wx - v_left) * scale = (500 - 200)*0.2 = 60;

let sy = s_height - (wy - v_bottom) * scale = 80 - (400 - 150)*0.2 = 30;

let screenPt = {sx, sy } = {60,30};

以上就是世界坐标到画布屏幕坐标换算的推导过程,世界坐标到屏幕坐标的换算用于显示。而当通过鼠标动作绘制图形或者拾取图形时,则涉及就是这个变换的逆变换-屏幕坐标到世界坐标变换,这是二维图形,变换过程比较简单,所以就不再讲推导过程了,直接给给出公式,下面假设画布上某个点的屏幕坐标为(sx,sy):

let scale = projectScale / scaling;

let wx = sx* scale + v_left;

let wy = (mapHgt - sy) * scale + v_top;

let worldPt = {wx, wy };

worldPt就是画布上点(sx,sy)对应的世界坐标。

下面是代码截图,MapViewParam表示当前画布上各种跟图形显示相关的参数,如投影缩放系数、图形缩放系数、视口区域、世界坐标区域、画布屏幕尺寸等;CoordTRFUtil则是坐标换算工具,里面包含了本文讲到的两种换算,其他换算函数后面章节中会进行讲解。

PS------本人文章中所涉及的所有图,除了屏幕截图之外都是使用本编辑器绘制、编辑的。专业版已发布在本人个人网站上:https://fanghuatech.cn

标签: 编辑器

本文转载自: https://blog.csdn.net/longx13/article/details/139562497
版权归原作者 longx13 所有, 如有侵权,请联系我们删除。

“从零手撕一个网页版图形编辑器之坐标变换(3)”的评论:

还没有评论