【问题标题】:Is this code drawing at the point or pixel level? How to draw retina pixels?此代码是在点级还是像素级绘制?如何绘制视网膜像素?
【发布时间】:2017-12-02 16:57:18
【问题描述】:

考虑一下这个绘制(圆形)渐变的令人赞叹的脚本,

https://github.com/paiv/AngleGradientLayer/blob/master/AngleGradient/AngleGradientLayer.m

int w = CGRectGetWidth(rect);
int h = CGRectGetHeight(rect);

然后

angleGradient(data, w, h ..

它会遍历所有这些

for (int y = 0; y < h; y++)
for (int x = 0; x < w; x++) {

基本上是设置颜色

    *p++ = color;

但是等等 - 这不是按点工作,而不是像素吗?

实际上,您会如何在密集屏幕上绘制物理像素?

是不是这样的:

假设设备上的密度为 4。就像在上面的代码中一样绘制,但是,在一个四倍大的位图上,然后把它放在矩形中?

这看起来很乱 - 但就是这样吗?

【问题讨论】:

  • 注意:如果你的问题实际上是绘制一个圆形渐变,有很多简单的方法,例如:stackoverflow.com/a/44263593/294884我使用“圆形渐变”只是作为像素绘制的一个例子。跨度>

标签: ios core-graphics pixel metal


【解决方案1】:

[注意:github 示例上的代码不是以像素为单位计算梯度。 github 示例上的代码基于点计算梯度。 -胖子]

代码以像素为单位工作。首先,它用像素颜色数据填充一个简单的光栅位图缓冲区。这显然没有像素以外的图像比例或单位的概念。接下来,它从该缓冲区创建一个CGImage(有点奇怪)。 CGImage 也没有像素以外的比例或单位的概念。

问题出现在CGImage 的绘制位置。此时是否进行缩放取决于图形上下文及其配置方式。上下文中有一个隐式转换,将用户空间(点,或多或少)转换为设备空间(像素)。

-drawInContext: 方法应该使用CGContextConvertRectToDeviceSpace() 转换矩形以获得图像的矩形。请注意,未转换的 rect 仍应用于调用 CGContextDrawImage()

因此,对于 2x Retina 显示上下文,原始矩形将以点为单位。假设是 100x200。图像矩形的大小将加倍以表示像素,200x400。绘制操作会将其绘制到 100x200 矩形,这似乎会缩小大型、高度详细的图像,从而丢失信息。但是,在内部,绘制操作会在进行实际绘制之前将目标矩形缩放到设备空间,并从 200x400 像素的图像中填充 200x400 像素的区域,保留所有细节。

【讨论】:

  • 如果您看一下 github 代码,请第 96 行。请注意,Pavel 实际上只是获得了点大小:因此,在您在答案中给出的示例中,这将是 100x200。 Pavel 不应该将该尺寸翻倍吗? 因此,Pavel 会将其翻倍至 200x400,如您的示例所示。 (假设密度为“2”。)也许我遗漏了一些东西:如果我没记错的话,Pavel 所做的完全不是你描述的那样?我错过了什么吗??
  • 我确实看过 AngleGradientLayer 代码。这就是我回答的依据。这就是我在提到-drawInContext: 方法时所说的。正如我所说,为了正确支持高分辨率,该代码应该在确定它创建的图像的大小时将从上下文中获取的矩形转换为设备空间。我想这与您的“方法 M”相对应。
  • 明白了! @kenthomases!代码应该转换矩形。对不起,我看错了你的答案。太棒了-史诗! :) 再次感谢。赏金来了....
【解决方案2】:

因此,根据 KenThomases 的出色回答和一天的测试,这就是您在物理像素级别上绘制的确切方法。我想。

class PixelwiseLayer: CALayer {

    override init() {

        super.init()
        // SET THE CONTENT SCALE AT INITIALIZATION TIME
        contentsScale = UIScreen.main.scale
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override open func draw(in ctx: CGContext) {

        let rectDEVICESPACE = ctx.convertToDeviceSpace(bounds).size
        // convertToDeviceSpace >>KNOWS ABOUT CONTENT SCALE<<
        // and YOU have CORRECTLY SET content scale at initialization time

        // write pixels to DEVICE SPACE, BUT ...
        let img = pixelByPixelImage(sizeInDeviceSpace: rectDEVICESPACE)

        // ... BUT the draw# call uses only the NORMAL BOUNDS
        ctx.draw(img, in: bounds)
    }

    private func pixelByPixelImage(sizeInDeviceSpace: CGSize) -> CGImage {

        let wPIXELS = Int(sizeInDeviceSpace.width)
        let hPIXELS = Int(sizeInDeviceSpace.height)
        // !!!THAT IS ACTUAL PIXELS!!!

        // you !!!DO NOT!!! need to multiply by UIScreen.main.scale,
        // as is seen in much example code.
        // convertToDeviceSpace does it properly.

        let bitsPerComponent: Int = MemoryLayout<UInt8>.size * 8
        let bytesPerPixel: Int = bitsPerComponent * 4 / 8
        let colorSpace: CGColorSpace = CGColorSpaceCreateDeviceRGB()
        let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedLast.rawValue)

        var data = [RGBA]()

        for y in 0..<hPIXELS {
            for x in 0..<wPIXELS {

                let c = yourPixelColor(atPoint: x .. y)
                data.append(c)
            }
        }

        // the context ... use actual pixels!!!!
        let ctx = CGContext(data: &data,
                    width: wPIXELS, height: hPIXELS,
                    bitsPerComponent: bitsPerComponent,
                    bytesPerRow: wPIXELS * bytesPerPixel,
                    space: colorSpace,
                    bitmapInfo: bitmapInfo.rawValue)
        let img = ctx?.makeImage()!
        return img!  // return a CGImage in actual pixels!!!!!!
    }

    // (PS, it's very likely you'll want needsDisplayOnBoundsChange as with most layers.
    // Set it true in init(), needsDisplayOnBoundsChange = true )

}

fileprivate struct RGBA { // (build raw data like this)
    var r: UInt8
    var g: UInt8
    var b: UInt8
    var a: UInt8
}

关键要素:

首先...

        super.init()
        // SET THE CONTENT SCALE >>>>AT INITIALIZATION TIME<<<<
        contentsScale = UIScreen.main.scale

第二...

    override open func draw(in ctx: CGContext) {

        realPixelSize = ctx.convertToDeviceSpace(bounds).size
        ...
    }

第三...

    override open func draw(in ctx: CGContext) {

        ...
        your image = yourPixelDrawingFunction( realPixelSize ) // NOT BOUNDS
        ctx.draw(img, in: bounds)  // NOT REALPIXELSIZE
    }

示例 ...

console:
contentsScale 3.0
UIScreen.main.scale 3.0
bounds (0.0, 0.0, 84.0, 84.0)
rectDEVICESPACE (252.0, 252.0)
actual pixels being created as data: w, h 252, 252

在初始化时设置contentsScale 是绝对关键的。

我尝试了一些操作系统版本,但不幸的是contentsScale 的图层默认值是“1”而不是屏幕密度,所以,不要忘记设置它! (请注意,操作系统中的其他系统也会使用它来了解如何有效地处理您的图层等)

【讨论】:

  • 我认为你不应该在 yourPixelDrawingFunction() 中乘以 UIScreen.main.scale
  • 您设置图层的contentsScale 为时已晚。在draw() 期间执行此操作意味着它不会影响传递给draw() 的上下文。此外,如果您正在编写自定义视图类而不是自定义图层类,则会为视图的 draw() 调用设置上下文及其在用户和设备空间之间的转换。
  • 你完全正确@KenThomases - 我现在看到了机制。
【解决方案3】:

听起来您正在寻找的是 UIScreen 上的 scale 属性:

https://developer.apple.com/documentation/uikit/uiscreen/1617836-scale

这允许您控制坐标系统为您提供的每个虚拟像素的像素数。 iOS 设备基本上工作在非视网膜坐标。旧链接解释这里发生了什么:

http://www.daveoncode.com/2011/10/22/right-uiimage-and-cgimage-pixel-size-retina-display/

不要使用他的宏,因为一些设备现在是 3.0 的规模,但帖子解释了发生了什么。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-01-30
    • 1970-01-01
    • 1970-01-01
    • 2012-08-13
    相关资源
    最近更新 更多