这段代码是在点还是像素级别上绘制的?如何绘制视网膜像素?

7
考虑这个优秀的脚本,它绘制了一个(圆形)渐变。

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。像上面的代码一样绘制,但是使用四倍大小的位图,然后将其放入矩形中?

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


注意:如果您的问题确实是绘制圆形渐变,那么有许多简单的方法,例如:https://dev59.com/mqLia4cB1Zd3GeqPeyqP#44263593。我只是将“圆形渐变”用作像素绘制的示例。 - Fattie
3个回答

5

[注意:github示例中的代码并不是基于像素计算梯度的。github示例中的代码是基于点计算梯度的。 -Fattie]

该代码是基于像素工作的。首先,它使用像素颜色数据填充一个简单的光栅化位图缓冲区。这显然没有任何关于图像比例或单位的概念,除了像素。接下来,它以有些奇怪的方式从该缓冲区创建CGImageCGImage也没有除了像素之外的比例或单位概念。

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

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

因此,对于2x Retina显示器上下文,原始矩形将以点为单位。比如说100x200。图像矩形将加倍,表示像素,200x400。绘图操作将把它绘制到100x200矩形中,这可能看起来会将大的、高度详细的图像缩小,丢失信息。但是,在实际绘制之前,绘图操作将把目标矩形缩放到设备空间,填充一个从200x400像素图像获取的200x400像素区域,保留所有细节。


请注意,如果您瞥一眼 Github 代码,第96行。请注意,Pavel实际上只获取了点的大小:因此在您在回答中给出的示例中,大小将是100x200。但是,Pavel不应该将其大小加倍吗?因此,Pavel将其加倍为200x400,就像您的示例一样。(假设密度为“2”)。也许我漏掉了什么:如果我没有错,Pavel正在做您所描述的事情的反面?我有没有漏掉什么? - Fattie
我确实查看了AngleGradientLayer代码。这就是我回答的基础。当我提到“-drawInContext:”方法时,我所说的就是它。正如我所说,为了正确支持高分辨率,该代码应在确定创建的图像大小时将其从上下文中获取的矩形转换为设备空间。我想这对应于您所说的“M方法”。 - Ken Thomases
明白了!@kenthomases!这段代码应该转换矩形。抱歉,我误读了你的回答。太棒了 - 史诗级!:) 再次感谢。奖励即将到来... - Fattie

1

因此,基于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 - Ken Thomases
你设置图层的contentsScale太晚了。在draw()期间这样做意味着它不能影响传递给draw()的上下文。此外,如果您正在编写自定义视图类而不是自定义图层类,则上下文及其用户和设备空间之间的变换将为视图的draw()调用设置。 - Ken Thomases
你是完全正确的 @KenThomases - 我现在明白了机制。 - Fattie

0

根据您的描述,您似乎正在寻找UIScreen上的比例属性:

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比例,但是这篇文章解释了发生了什么。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接