图形的另一种操作就是变换,主要包括平移、缩放和旋转等形式变换。变换离不开坐标,不同的绘图系统对于坐标系的定义也有所区别。
在苹果的2D图形技术是Quartz 2D和UIKit,Quartz 2D是Mac OS X和iOS环境下的2D绘图引擎。涉及内容包括:基于路径的绘图,透明度绘图,遮盖,阴影,透明层,颜色管理,防锯齿渲染,生成PDF,以及PDF元数据相关处理。在iOS中还可以通过UIKit进行图形绘制,但是Quartz 2D和UIKit坐标系不同。
图2-14 Quartz 2D的坐标系
Quartz 2D的坐标系,原点在左下角, x 方向向右为正方向, y 方向向上为正方向,如图2-14所示。
图2-15 UIKit的坐标系
UIKit的坐标系,原点在左上角, x 方向向右为正方向, y 方向向下为正方向,如图2-15所示。
图2-16 原始图片
下面通过实例介绍他们的区别,图2-16所示是一张可爱的招财猫图片。
如果绘制这样的图片在不同坐标系下会有什么不同呢?绘制图片可以通过2.2节介绍的UIImage类中几个绘制图像的方法实现,这种方式就不再介绍了。下面看看实现方式。
- (void)drawRect:(CGRect)rect { NSString *path = [[NSBundle mainBundle] pathForResource:@"cat" ofType:@"png"]; UIImage *img = [UIImage imageWithContentsOfFile:path]; CGImageRef image = img.CGImage; CGContextRef context = UIGraphicsGetCurrentContext(); CGContextSaveGState(context); CGRect touchRect = CGRectMake(0, 0, img.size.width, img.size.height); CGContextDrawImage(context, touchRect, image); CGContextRestoreGState(context); }
图2-17 绘制图片
上面的实现代码方式全部通过Quartz 2D函数实现绘制,也可以实现绘制图像的目的,其中CGContextDrawImage函数相当于UIImage类中的绘制图像方法。但是绘制出来的结果令人沮丧,图像倒过来了(如图2-17所示),这是因为Quartz 2D坐标系和UIKit坐标系不同所导致的,我们使用的绘制函数都是基于Quartz 2D坐标系的。
为了能够正确的显示在UIKit坐标系下,需要进行坐标变换,需要修改代码如下。
- (void)drawRect:(CGRect)rect { NSString *path = [[NSBundle mainBundle] ' pathForResource:@"cat" ofType:@"png"]; UIImage *img = [UIImage imageWithContentsOfFile:path]; CGImageRef image = img.CGImage; CGContextRef context = UIGraphicsGetCurrentContext(); CGContextSaveGState(context); CGContextTranslateCTM(context, 0, img.size.height); ① CGContextScaleCTM(context, 1, -1); ② CGRect touchRect = CGRectMake(0, 0, img.size.width, img.size.height); CGContextDrawImage(context, touchRect, image); CGContextRestoreGState(context); }
图2-18 变换之后
我们添加了第①~②行代码,具体的含义我们先不用理解,这关于坐标变换的问题,会在2.4节介绍。添加之后的运行结果如图2-18所示。
提示: 读者可能已经发现,如果使用UIImage类提供的绘制图像方法不会出现这个问题,UIImage采用的坐标是UIKit坐标,已经进行了转换,不需要再进行转换了。
在图形变换过程中需要大量使用矢量、矩阵及其运算,学习图形变换需要了解这些基本矩阵知识,关于这些知识本书不再介绍。2D图形的基本变换包括:平移、缩放和旋转三种变换。
图2-19 平移变换
平移是一物体从一个位置到另一位置所做的直线移动。如果要把一个位于 P ( x , y )的点移到新位置 P ′( x ′, y ′)时,只要在原坐标上加上平移距离Tx及Ty即可,如图2-19所示。
图2-20 缩放变换
用来改变一物体大小的变换称为缩放变换。如果要对一个多边形进行比例变换,那么可把各顶点的坐标( x , y )均乘以比例因子 S x 、 S y ,以产生变换后的坐标( x ′, y ′)。其中, S x 及 S y 可以是任意正数, S x 、 S y 可以相等或不等。如果比例因子数值小于1,则物体尺寸减小;大于1,则使物体放大;Sx及Sy都等于1,则物体大小形状不变。需要注意的是图2-20表示的比例变换是针对坐标原点的。
图2-21 旋转变换
物体上的各点绕一固定点沿圆周路径做转动称为旋转变换。我们可用旋转角表示旋转量的大小。一个点由位置( x 、 y )旋转到( x ′, y ′)的角度为自水平轴算起的角度, θ 为旋转角,如图2-21所示。
有的图形系统还提供另外几种很有用的变换,如反射变换及错切变换等。我们重点介绍一下反射变换。反射是用来产生物体的镜像的一种变换。物体的镜像一般是相对于一个对称轴生成的,因此反射变换可以分为 x 轴对称变换、 y 轴对称变换和坐标原点的对称变换。
图2-22 x 轴对称变换
x 轴的对称变换是一种特殊形式的缩放变换,其中 S x =1, S y =-1,如图2-22所示。
图2-23 y 轴对称变换
y 轴的对称变换是一种特殊形式的缩放变换,其中 S x =-1, S y =1,如图2-23所示。
图2-24 坐标原点的对称变换
关于坐标原点的对称变换是一种特殊形式的缩放变换,其中 S x =-1, S y =-1,如图2-24所示。
Quartz 2D提供了多种形式的变换,其中主要是当前变换矩阵变换(current transformation matrix,CTM)和仿射(affine)变换。CTM变换,这种变换比较简单,主要的函数有:
平移变换根据指定的 T x 、 T y 值移动坐标系统的原点。我们通过调用CGContextTranslateCTM函数来改变每个点的 x , y 坐标值。如图2-25右所示显示了一幅图片沿 x 轴移动了100个单位,沿y轴移动了50个单位。具体代码如下:
- (void)drawRect:(CGRect)rect { NSString *path = [[NSBundle mainBundle] pathForResource:@"cat" ofType:@"png"]; UIImage *img = [UIImage imageWithContentsOfFile:path]; CGImageRef image = img.CGImage; CGContextRef context = UIGraphicsGetCurrentContext(); CGContextSaveGState(context); CGContextTranslateCTM (context, 100, 50); CGRect touchRect = CGRectMake(0, 0, img.size.width, img.size.height); CGContextDrawImage(context, touchRect, image); CGContextRestoreGState(context); } @end
![]() |
![]() |
图2-25 平移变换
缩放操作根据指定的 S x 、 S y 因子来改变图像的大小,从而放大或缩小图像。 S x 、 S y 因子的大小决定了新的坐标系是否比原始坐标系大或者小。图2-26显示了指定 S x 因子为0.5, S y 因子为0.75后的缩放效果。具体代码如下:
- (void)drawRect:(CGRect)rect { NSString *path = [[NSBundle mainBundle] pathForResource:@"cat" ofType:@"png"]; UIImage *img = [UIImage imageWithContentsOfFile:path]; CGImageRef image = img.CGImage; CGContextRef context = UIGraphicsGetCurrentContext(); CGContextSaveGState(context); CGContextScaleCTM (context, .5, .75); CGRect touchRect = CGRectMake(0, 0, img.size.width, img.size.height); CGContextDrawImage(context, touchRect, image); CGContextRestoreGState(context); } @end
![]() |
![]() |
图2-26 缩放变换
另外,通过指定 S x 因子为负数是 x 轴对称变换,同样可以指定 S y 因子为负数是 y 轴对称变换。通过调用CGContextScaleCTM函数来指定 S x 、 S y 缩放因子。
S x 、 S y 旋转变换根据指定的角度来旋转坐标。我们可以通过CGContextRotateCTM函数来指定旋转角度(以弧度为单位)。图2-27右图所示,显示了图片以原点为中心顺时针旋转45°,代码如下所示。
- (void)drawRect:(CGRect)rect { NSString *path = [[NSBundle mainBundle] pathForResource:@"cat" ofType:@"png"]; UIImage *img = [UIImage imageWithContentsOfFile:path]; CGImageRef image = img.CGImage; CGContextRef context = UIGraphicsGetCurrentContext(); CGContextSaveGState(context); CGContextRotateCTM(context, radians(45.)); CGRect touchRect = CGRectMake(0, 0, img.size.width, img.size.height); CGContextDrawImage(context, touchRect, image); CGContextRestoreGState(context); } @end
![]() |
![]() |
图2-27 旋转变换
其中,radians是我们定义的宏,用来将弧度转化成为度。我们需要h文件中添加如下代码。
#define radians(x) (x*M_PI/180)
由于旋转操作使图片的部分区域置于上下文之外,所以区域外的部分被裁减。如果旋转的弧度为负数,则图形是逆时针旋转。
CGContextRotateCTM(context, radians(-45.));
有些情况下需要组合变换,得到累加效果。还记得2.4.1一节Quartz 2D和UIKit坐标系不同导致的图片倒置吗?如图2-28右所示的效果,需要组合变化。具体变化代码如下。
- (void)drawRect:(CGRect)rect { NSString *path = [[NSBundle mainBundle] pathForResource:@"cat" ofType:@"png"]; UIImage *img = [UIImage imageWithContentsOfFile:path]; CGImageRef image = img.CGImage; CGContextRef context = UIGraphicsGetCurrentContext(); CGContextSaveGState(context); CGContextTranslateCTM(context, 0, img.size.height); CGContextScaleCTM(context, 1, -1); CGRect touchRect = CGRectMake(0, 0, img.size.width, img.size.height); CGContextDrawImage(context, touchRect, image); CGContextRestoreGState(context); }
其中,先进行了平移变换,再进行了缩放变换,CGContextScaleCTM(context,1,-1)缩放的结果是进行了 y 轴对称变换。要想达到这个效果上面的组合方式不是唯一的。
![]() |
![]() |
图2-28 组合变换
提示: 当相同的绘制程序在一个UIView对象和Quartz图形上下文上进行绘制时候,需要做一个变换,使Quartz图形上下文与UIView具有相同的坐标系。要达到这一目的,需要将Quartz图形上下文的原点平移到左上角,再乘以-1对( y 轴对称变换),如图2-29显示了这种转换过程。
图2-29 坐标转换
仿射(affine)变换也是一种2D坐标变换,它可以重用变换,经过多次变换(即多次的矩阵相乘),每一种变换都可以用矩阵表示,通过多次矩阵相乘得到最后结果。仿射变换函数:
如图2-28右所示的效果,可以使用仿射变换,具体变化代码如下。
- (void)drawRect:(CGRect)rect { NSString *path = [[NSBundle mainBundle] pathForResource:@"cat" ofType:@"png"]; UIImage *img = [UIImage imageWithContentsOfFile:path]; CGImageRef image = img.CGImage; CGContextRef context = UIGraphicsGetCurrentContext(); CGContextSaveGState(context); CGAffineTransform myAffine = CGAffineTransformMakeTranslation(0, img.size.height); ① myAffine = CGAffineTransformScale(myAffine, 1, -1); ② CGContextConcatCTM(context, myAffine); ③ CGRect touchRect = CGRectMake(0, 0, img.size.width, img.size.height); CGContextDrawImage(context, touchRect, image); CGContextRestoreGState(context); }
首先,通过第①行所示CGAffineTransformMakeTranslation函数创建新的平移变换矩阵。第②行代码是通过CGAffineTransformScale(myAffine,1,-1)语句在平移变换矩阵上乘以缩放变换矩阵,然后再通过第③行所示CGContextConcatCTM函数连接到CTM矩阵并输出结果。