底部的编辑/添加
听起来您可能对坐标系如何映射到像素网格感到困惑。当您绘制到 CGContext 中时,您正在绘制到一个“连续的”基于浮点的平面中。这个连续平面被映射到显示器的像素网格上,使得整数值落在屏幕像素之间的线上。
理论上,在任何给定设备的默认比例下(非视网膜为 1.0,视网膜为 2.0),如果您从 0,0 -> 320,480 绘制一个矩形,宽度为 100% 的不透明度黑色笔触1.0pt,你可以期待以下结果:
- 在非视网膜上,您将在外部有一个 1 像素宽的矩形
屏幕的不透明度为 50%。
- 在视网膜上,你会有一个 1 像素
屏幕外部的宽矩形,不透明度为 100%。
这是因为零线正好位于显示器的边缘,在显示器中的第一行/列像素和不在显示器中的第一行/列像素之间。在这种情况下,当您以默认比例绘制一条 1pt 线时,它会沿着路径的中心绘制,因此一半会从显示屏上绘制出来,并且在您的连续平面中,您将有一条线的宽度从 - 0.5pt 到 0.5pt。在非 Retina 显示器上,这将变成 50% 不透明度的 1px 线,而在 Retina 显示器上,将呈现为 100% 不透明度的 1px 线。
编辑
OP 说:
我的目标是在视网膜显示屏上绘制一些形状,并且每一行
将是 1 像素宽度和 100% 不透明度。
这个目标不需要关闭抗锯齿功能。如果您在启用抗锯齿的情况下看到水平和垂直线上的 alpha 混合,那么您没有在正确的位置或以正确的尺寸绘制线条。
在这种情况下,在函数中:CGContextMoveToPoint(context, X, Y);
在 X 轴上的 2 个像素之间,引擎将选择正确的一个,然后在
Y 轴它会选择较高的那个。但在函数中:
CGContextFillRect(context, someRect);它会像地图一样填充
像素网格(1 比 1)。
您看到这种令人困惑的行为的原因是您关闭了抗锯齿功能。能够看到和理解这里发生的事情的关键是保持抗锯齿打开,然后进行必要的更改,直到你得到它恰到好处。开始执行此操作的最简单方法是保持 CGContext 的变换与默认值保持不变,并更改您传递给绘图例程的值。确实,您也可以通过转换 CGContext 来完成其中的一些工作,但这会增加一个数学步骤,该步骤是在您传递给任何 CG 例程的每个坐标上完成的,您无法在调试器中单步执行,所以我高度建议您从标准变换开始并保留 AA。在尝试弄乱上下文变换之前,您需要全面了解 CG 绘图的工作原理。
是否有一些 Apple 定义的方法可以将图形映射到像素而不是像素
像素之间的线?
总而言之,“不”,因为 CGContext 不是通用的位图(例如,您可能正在绘制 PDF —— 您无法通过询问 CGContext 知道)。您正在绘制的平面本质上是一个浮点平面。这就是它的工作原理。使用浮点平面完全可以实现 100% 像素精度的位图绘制,但要做到这一点,您必须了解这些东西的实际工作原理。
您可能可以通过获取给定的默认 CGContext 并进行此调用来更快地启动:
CGContextTranslateCTM(context, 0.5, 0.5);
这将做的是将 (0.5, 0.5) 添加到您传递给所有后续绘图调用的每个点(直到您调用 CGContextRestoreGState)。如果您没有对上下文进行其他更改,那么当您从 0,0 -> 10,0 绘制一条线时,即使启用了抗锯齿,它也会完全对齐像素。 (请参阅我上面的初步回答以了解为什么会这样。)
使用这个 0.5、0.5 的技巧可能会让您更快地开始,但是如果您想使用像素精确的 CG 绘图,您确实需要了解基于浮点的图形上下文是如何工作的,以及它们之间的关系到可能(或可能不)支持它们的位图。
关闭 AA,然后微调值直到它们“正确”,这只是在以后自找麻烦。例如,假设有一天 UIKit 传递给你一个上下文,它的变换有不同的翻转?在这种情况下,您的一个值可能会向下取整,它曾经向上取整(因为现在它在应用翻转时乘以 -1.0)。同样的问题也可能发生在被转化为负象限的上下文中。此外,您不知道(并且不能严格依赖版本到版本)当您关闭 AA 时 CoreGraphics 将使用什么舍入规则,所以如果您最终因任何原因而被交给不同 CTM 的上下文会得到不一致的结果。
为了记录,您可能想要关闭抗锯齿的时间是在您绘制非直线路径并且您不希望 CoreGraphics 对边缘进行 alpha 混合时。您可能确实想要这种效果,但是关闭 AA 会使学习、理解和确定正在发生的事情变得更加困难,所以我强烈建议您将其保持打开状态,直到您完全理解这些内容,并且将所有直线绘图都完美像素化对齐。 然后拨动开关以消除非直线路径上的 AA。
关闭抗锯齿并不神奇地允许将 CGContext 处理为位图。它采用浮点平面并添加了一个您看不到(代码方面)、无法控制且难以理解和预测的舍入步骤。最后,你还有一个浮点平面。
只是一个简单的例子来帮助澄清:
- (void)drawRect:(CGRect)rect
{
CGContextRef ctx = UIGraphicsGetCurrentContext();
CGContextSaveGState(ctx);
BOOL doAtDefaultScale = YES;
if (doAtDefaultScale)
{
// Do it by using the right values for the default context
CGContextSetStrokeColorWithColor(ctx, [[UIColor blackColor] CGColor]);
CGContextSetLineWidth(ctx, 0.5); // We're working in scaled pixel. 0.5pt => 1.0px
CGContextStrokeRect(ctx, CGRectMake(25.25, 25.25, 50, 50));
}
else
{
// Do it by transforming the context
CGContextSetStrokeColorWithColor(ctx, [[UIColor blackColor] CGColor]);
CGContextScaleCTM(ctx, 0.5, 0.5); // Back out the default scale
CGContextTranslateCTM(ctx, 0.5, 0.5); // Offset from edges of pixels to centers of pixels
CGContextSetLineWidth(ctx, 1.0); // we're working in device pixels now, having backed out the scale.
CGContextStrokeRect(ctx, CGRectMake(50, 50, 100, 100));
}
CGContextRestoreGState(ctx);
}
创建一个新的单视图应用程序,使用此 drawRect: 方法添加自定义视图子类,并在 .xib 文件中设置默认视图以使用您的自定义类。这个 if 语句的两边在视网膜显示器上产生相同的结果:一个 100x100 设备像素、非 alpha 混合的正方形。第一面通过使用默认比例的“正确”值来做到这一点。它的另一面通过退出 2x 比例,然后将平面从与设备像素的边缘对齐到与设备像素的中心对齐来实现。注意笔画宽度的不同(比例因子也适用于它们。)希望这会有所帮助。
OP 回复:
但请注意,有一点阿尔法混合。这是
3200 倍缩放截图:
不,真的。相信我。这是有原因的,它不在上下文中打开了抗锯齿。 (另外,我认为您的意思是 3200% 变焦,而不是 3200 倍变焦——在 3200 倍变焦时,单个像素不适合 30 英寸的显示器。)
在我给出的示例中,我们绘制了一个矩形,所以我们不需要考虑线的结尾,因为它是一条封闭的路径——这条线是连续的。既然您正在绘制单个线段,您确实必须考虑行尾以避免 alpha 混合。这是“像素边缘”与“像素中心”的回归。默认线帽样式是 kCGLineCapButt。 kCGLineCapButt 表示线条的末端正好从您开始绘制的位置开始。如果你想让它表现得更像钢笔——也就是说,如果你把一支毡尖笔放下,打算在右边画一条线 10 个单位,一些墨水会从左边渗出。 exact 您将笔指向的点 - 您可以考虑使用 kCGLineCapSquare (或 kCGLineCapRound 用于圆形末端,但对于单像素级绘图只会让您对 alpha 混合感到疯狂,因为它会将 alpha 计算为 1.0 - 0.5/pi)。我在 Apple 的 Quartz 2D Programming Guide 的插图上覆盖了假设的像素网格,以说明行尾与像素的关系:
但是,我离题了。这是一个例子;考虑以下代码:
- (void)drawRect:(CGRect)rect
{
CGContextRef ctx = UIGraphicsGetCurrentContext();
CGContextSaveGState(ctx);
CGContextSetStrokeColorWithColor(ctx, [[UIColor blackColor] CGColor]);
CGContextSetLineWidth(ctx, 0.5); //On device pixel at default scale
CGContextMoveToPoint(ctx, 2.0, 2.25); // Edge of pixel in X, Center of pixel in Y
CGContextAddLineToPoint(ctx, 7.0, 2.25); // Draw a line of 5 scaled, 10 device pixels
CGContextStrokePath(ctx); // Stroke it
CGContextRestoreGState(ctx);
}
请注意,我没有移至 2.25、2.25,而是移至 2.0、2.25。这意味着我在 X 维度中位于像素的边缘,在 Y 维度中位于像素的中心。因此,我不会在行尾进行 alpha 混合。事实上,在 Acorn 中放大到 3200%,我看到以下内容:
现在,是的,在某些时候,如果您正在转换值或使用较长的计算。我已经看到在非常复杂的情况下像 0.000001 一样严重的错误会出现,如果您谈论的是 1.999999 与 2.000001 之间的差异之类的情况,即使是这样的错误也会咬到您,但这不是您在这里看到的.
如果您的目标只是绘制像素精确的位图,仅由轴对齐/直线元素组成,则有否理由关闭上下文抗锯齿。在这种情况下,在我们讨论的缩放级别和位图密度下,您应该可以轻松避免几乎所有由 1e-6 或更小的浮点错误引起的问题。 (事实上,在这种情况下,任何小于一个像素的 1/256 的东西都不会对 8 位上下文中的 alpha 混合产生任何影响,因为当量化为 8 位时,此类误差实际上为零。)
让我借此机会推荐一本书:Programming with Quartz: 2D and PDF Graphics in Mac OS X 这是一本很棒的书,并且非常详细地涵盖了所有这些内容。