I'm trying to customize grouped UITableViewCell's backgroundView with a gradient, based on code I find on this blog. It's a subclass of UIView for use on cell.backgroundView.
The colors of the background's gradient are defined like this on the original code :
#define TABLE_CELL_BACKGROUND { 1, 1, 1, 1, 0.866, 0.866, 0.866, 1} // #FFFFFF and #DDDDDD
And then, used like this on the drawRect of the subclassed backgroundView:
CGFloat components[8] = TABLE_CELL_BACKGROUND;
myGradient = CGGradientCreateWithColorComponents(myColorspace, components , locations, 2);
I'm trying to implement a function to set start and end color for the gradient, which takes two UIColors and then fill in a global float array float startAndEndColors[8] (in .h / #interface) for later use:
-(void)setColorsFrom:(UIColor*)start to:(UIColor*)end{
float red = 0.0, green = 0.0, blue = 0.0, alpha =0.0, red1 = 0.0, green1 = 0.0, blue1 = 0.0, alpha1 =0.0;
[start getRed:&red green:&green blue:&blue alpha:&alpha];
[end getRed:&red1 green:&green1 blue:&blue1 alpha:&alpha1];
//This line works fine, my array is successfully filled, just for test
float colorsTest[8] = {red, green, blue, alpha, red1, green1, blue1, alpha1};
//But for this one, I just have an error.
//"Expected expression"
// \
// v
startAndEndColors = {red, green, blue, alpha, red1, green1, blue1, alpha1};
}
But it throw me this error "Expected expression" at assignation.
I tried with CGFloat, desperately adding random const, but I quickly ran out of ideas.
I simply don't get it, why can't I fill my float array this way? What am I doing wrong?
Comment added as answer:
The only way of creating an array that way is dynamically in code. If you are adding to an iVar (class variable) you need to go through one by one because the memory has already been allocated at initialization. So use startAndEndColors[0] = ..., etc.
As for your follow up question: No, there is no way to assign values in that way to memory that has already been initialized in the allocation phase. If you used std::vector or other objects then it would be possible.
A way around that would be something like this in your header
CGFloat *startAndEndColors;
And then something like this in your implementation
float colorsTest[8] = {red, green, blue, alpha, red1, green1, blue1, alpha1};
startAndEndColors = colorsTest;
That way you can initialize it the way you want to, but you have no guarantee of the number of objects in your startAndEndColors object. You could later assign it to something of the wrong size and cause crashes if you try to access outside of it's bounds.
Related
I'm building a real-time photo editor based on CIFilters and MetalKit. But I'm running into an issue with displaying wide gamut images in a MTKView.
Standard sRGB images display just fine, but Display P3 images are washed out.
I've tried setting the CIContext.render colorspace as the image colorspace, and still experience the issue.
Here are snippets of the code:
guard let inputImage = CIImage(mtlTexture: sourceTexture!) else { return }
let outputImage = imageEditor.processImage(inputImage)
print(colorSpace)
context.render(outputImage,
to: currentDrawable.texture,
commandBuffer: commandBuffer,
bounds: inputImage.extent,
colorSpace: colorSpace)
commandBuffer?.present(currentDrawable)
let pickedImage = info[UIImagePickerControllerOriginalImage] as! UIImage
print(pickedImage.cgImage?.colorSpace)
if let cspace = pickedImage.cgImage?.colorSpace {
colorSpace = cspace
}
I have found a similar issue on the Apple developer forums, but without any answers: https://forums.developer.apple.com/thread/66166
In order to support the wide color gamut, you need to set the colorPixelFormat of your MTKView to either BGRA10_XR or bgra10_XR_sRGB. I suspect the colorSpace property of macOS MTKViews won't be supported on iOS because color management in iOS is not active but targeted (read Best practices for color management).
Without seeing your images and their actual values, it is hard to diagnose, but I'll explain my findings & experiments. I suggest you start like I did, by debugging a single color.
For instance, what's the reddest point in P3 color space? It can be defined through a UIColor like this:
UIColor(displayP3Red: 1, green: 0, blue: 0, alpha: 1)
Add a UIButton to your view with the background set to that color for debugging purposes. You can either get the components in code to see what those values become in sRGB,
var fRed : CGFloat = 0
var fGreen : CGFloat = 0
var fBlue : CGFloat = 0
var fAlpha : CGFloat = 0
let c = UIColor(displayP3Red: 1, green: 0, blue: 0, alpha: 1)
c.getRed(&fRed, green: &fGreen, blue: &fBlue, alpha: &fAlpha)
or you can use the Calculator in macOS Color Sync Utility,
Make sure you select Extended Range, otherwise the values will be clamped to 0 and 1.
So, as you can see, your P3(1, 0, 0) corresponds to (1.0930, -0.2267, -0.1501) in extended sRGB.
Now, back to your MTKView,
If you set the colorPixelFormat of your MTKView to .BGRA10_XR, then you obtain the brightest red if the output of your shader is,
(1.0930, -0.2267, -0.1501)
If you set the colorPixelFormat of your MTKView to .bgra10_XR_sRGB, then you obtain the brightest red if the output of your shader is,
(1.22486, -0.0420312, -0.0196301)
because you have to write a linear RGB value, since this texture format will apply the gamma correction for you. Be careful when applying the inverse gamma, since there are negative values. I use this function,
let f = {(c: Float) -> Float in
if fabs(c) <= 0.04045 {
return c / 12.92
}
return sign(c) * powf((fabs(c) + 0.055) / 1.055, 2.4)
}
The last missing piece is creating a wide gamut UIImage. Set the color space to CGColorSpace.displayP3 and copy the data over. But what data, right? The brightest red in this image will be
(1, 0, 0)
or (65535, 0, 0) in 16-bit ints.
What I do in my code is using .rgba16Unorm textures to manipulate images in displayP3 color space, where (1, 0, 0) will be the brightest red in P3. This way, I can directly copy over its contents to a UIImage. Then, for displaying, I pass a color transform to the shader to convert from P3 to extended sRGB (so, not saturating colors) before displaying. I use linear color, so my transform is just a 3x3 matrix. I set my view to .bgra10_XR_sRGB, so the gamma will be applied automatically for me.
That (column-major) matrix is,
1.2249 -0.2247 0
-0.0420 1.0419 0
-0.0197 -0.0786 1.0979
You can read about how I generated it here: Exploring the display-P3 color space
Here's an example I built using UIButtons and an MTKView, screen-captured on an iPhoneX,
The button on the left is the brightest red on sRGB, while the button on the right is using a displayP3 color. At the center, I placed an MTKView that outputs the transformed linear color as described above.
Same experiment for green,
Now, if you see this on a recent iPhone or iPad, you should see the both the square in the center and the button to the right have the same bright colors. If you see this on a Mac that can't display them, the left button will appear the same color. If you see this in a Windows machine or a browser without proper color management, the left button may also appear to be of a different color, but that's only because the whole image is interpreted as sRGB and obviously those pixels have different values... But the appearance won't be correct.
If you want more references, check the testP3UIColor unit test I added here: ColorTests.swift,
my functions to initialize the UIImage: Image.swift,
and a sample app to try out the conversions: SampleColorPalette
I haven't experimented with CIImages, but I guess the same principles apply.
I hope this information is of some help. It also took me long to figure out how to display colors properly because I couldn't find any explicit reference to displayP3 support in the Metal SDK documentation.
I am trying to draw a circle shaded with a gradient from white to transparent. I am using Core Graphics.
Here is what I have which draws a gradient from white to black:
let colorSpace = CGColorSpaceCreateDeviceRGB();
let colors = [UIColor.white.cgColor, UIColor.black.cgColor] as CFArray;
let locations : [CGFloat] = [0.0, 1.0];
let glowGradient : CGGradient = CGGradient.init(colorsSpace: colorSpace, colors: colors, locations: locations)!;
let ctx = UIGraphicsGetCurrentContext()!;
ctx.drawRadialGradient(glowGradient, startCenter: rectCenter, startRadius: 0, endCenter: rectCenter, endRadius: imageWidthPts/2, options: []);
However, I do not want to draw white-to-black; I want to draw white-to-transparent.
To do so, I first tried changing the end color to UIColor.white.cgColor.copy(alpha: 0.0) (i.e., transparent white). However, this failed with:
fatal error: unexpectedly found nil while unwrapping an Optional value
I assume this error is due to the color being outside the specified RGB color space (CGColorSpaceCreateDeviceRGB()).
The fix would seem to be to change the specified color space to one with an alpha component, such as RGBA. However, such color spaces do not appear to exist! There are only CGColorSpaceCreateDeviceRGB, CGColorSpaceCreateDeviceCMYK, and CGColorSpaceCreateDeviceGray.
But it makes no sense for there to be no available color spaces with an alpha component. The documentation explicitly describes support for alpha in gradients. The documentation for CGGradient.init says:
For example, if the color space is an RGBA color space and you want to use two colors in the gradient (one for a starting location and another for an ending location), then you need to provide 8 values in components—red, green, blue, and alpha values for the first color, followed by red, green, blue, and alpha values for the second color.
This RGBA encoding makes perfect sense, but it's impossible to tell Core Graphics that I'm using such an RGBA encoding, because there is no RGBA color space!
Where is the CGColorSpace for RGBA?
You don't need the RGBA color space to draw transparent radial/linear gradients. The RGB color space is enough. If it's not drawing it transparently, you probably have the background color of the view or the context misconfigured.
If you're creating a context you want to make sure that you pass in false for opaque: Iphone How to make context background transparent?
If you're using a CALayer on a UIView, you need to make sure that the UIView's background color is set to UIColor.clear. If it's set to nil, you'll end up with the gradient blending with black instead.
A slightly unsatisfactory answer: you can get the color space of a context with .colorSpace. In my case, it seems to give me an RGBA space, but I can't see any guarantee of this.
Here's the gradient using .colorSpace:
let ctx = UIGraphicsGetCurrentContext()!;
let colorSpace = ctx.colorSpace!;
let colorComponents : [CGFloat] = [
// R G B A
1.0, 1.0, 1.0, 1.0,
1.0, 1.0, 1.0, 0.0,
];
let locations : [CGFloat] = [0.0, 1.0];
let glowGradient : CGGradient = CGGradient.init(
colorSpace: colorSpace,
colorComponents: colorComponents,
locations: locations,
count: locations.count
)!;
ctx.drawRadialGradient(glowGradient, startCenter: rectCenter, startRadius: 0, endCenter: rectCenter, endRadius: imageWidthPts/2, options: []);
It's particularly confusing that in my case, the colorSpace.numberOfComponents evaluates to 3, i.e. not RGBA, and yet it still correctly interprets the alpha component in the gradient. ¯\_(ツ)_/¯
The function that apple has already put in the phone that is in general>accessibility>invert colors, can I somehow use that in my program so for say when the user touches the screen the colors invert?
I don't know of a way to do this automatically, but you could invert colors yourself using an extension on UIColor and accessing the subviews?
extension UIColor {
var inverted: UIColor {
var r: CGFloat = 0.0, g: CGFloat = 0.0, b: CGFloat = 0.0, a: CGFloat = 0.0
self.getRed(&r, green: &g, blue: &b, alpha: &a)
return UIColor(red: (1 - r), green: (1 - g), blue: (1 - b), alpha: a) // Assuming you want the same alpha value.
}
}
And then if you want to update specific properties of the views you could do something like this:
view.subviews.map { $0.backgroundColor = $0.backgroundColor.invertedColor }
// And so on to change things like the tint color, text color, etc.
Sorry, I don't know a way to do this directly but till then this is better than nothing I guess.
I know of no such API call, and frankly I'd be surprised if it was available. Generally, Apple does not provide system-wide settings to individual applications. However, you could implement this yourself—but only for your own app.
Now this can be done with a buid-in method from SwiftUI; colorInvert().
The colorInvert() modifier inverts all of the colors in a view so that each color displays as its complementary color. For example, blue converts to yellow, and white converts to black.
See: https://developer.apple.com/documentation/swiftui/view/colorinvert()
Apple has made it very simple to to make linear and radial gradients, but is it possible to have the color of the gradient be set by a definable function? In my situation I want to make the fill color of an object to vary with a sinus function along the x-axis. It is not hard to make pngs and use them as patterns instead, but I just wonder if it is possible to make gradients where the red, green and blue components vary along certain axis with a sinus function instead.
Any answer is appreciated. Thanks in advance.
When you create the gradient using the CAGradientLayer class, you can use the colors property with a large number of colors and vary the color components according to the sine function. There will be linear interpolation between each pair of consecutive colors which will, however, not be noticeable when the number of colors (and locations) is large enough.
Here is an example that draws a sine gradient that variates back and forth between red and blue.
// Compute the colors using a sine step-size
let samples = 100
var colors = [CGColor]()
for i in 0..<samples {
let component = CGFloat(0.5 + sin(Double(i) / Double(samples - 1) * 4.0 * M_PI) / 2.0)
colors.append(UIColor(red: component, green: 0, blue: 1 - component, alpha: 1).CGColor)
}
// Create the gradient layer
let gradientLayer: CAGradientLayer = CAGradientLayer()
gradientLayer.colors = colors
// Install the gradient layer
gradientLayer.frame = self.view.bounds
self.view.layer.insertSublayer(gradientLayer, atIndex: 0)
The end result looks like this.
I'm an Objective-C noob, have searched high and low not finding the answer to this yet:
In my RubyMotion project I have a UIView subclass called StatusGuage, which contains a method called drawLinearGradient as follows:
def drawLinearGradient(context, rect, startColor, endColor)
colorspace = CGColorSpaceCreateDeviceRGB()
locations = [0.0, 1.0]
# colors = NSArray.arrayWithObjects(startColor, endColor, nil)
# ptrColors = Pointer.new(:object, colors)
colors = [startColor, endColor, nil]
# CGGradientRef gradient = CGGradientCreateWithColors(colorSpace, (CFArrayRef) colors, locations);
CGGradientCreateWithColors(colorspace, colors, locations)
end
I'd like to know how to call CGGradientCreateWithColors. It clearly expects a (CFArrayRef) pointer but I cannot figure out how to pass that in. One of the iterations that I've tried is commented out.
Here is the error message:
2012-05-11 16:57:36.331 HughesNetMeter[34906:17903]
*** Terminating app due to uncaught exception 'TypeError',
reason: 'status_guage.rb:43:in `drawLinearGradient:': expected
instance of Pointer, got `[0.0, 1.0]' (Array) (TypeError)
from status_guage.rb:13:in `drawRect:'
Thanks for any help.
A couple of things. The error is not talking about the colours, it is referring to the const CGFloat locations[] argument.
This should be a pointer which can be achieved like this (Reference on Pointer class)
locations = Pointer.new(:float, 2)
locations[1] = 1.0
Next up your array does not need the nil termination. In Ruby this would create an array with 3 objects, which is not what you want as it will most likely cause the CGGradientCreateWithColors() function to mess up
This looks like the example from http://www.raywenderlich.com/, so here's the rest from that
def drawLinearGradient(context, rect, startColor, endColor)
colorspace = CGColorSpaceCreateDeviceRGB()
locations = Pointer.new(:float, 2)
locations[1] = 1.0
colors = [startColor, endColor]
gradient = CGGradientCreateWithColors(colorspace, colors, locations)
startPoint = CGPointMake(CGRectGetMidX(rect), CGRectGetMinY(rect))
endPoint = CGPointMake(CGRectGetMidX(rect), CGRectGetMaxY(rect))
CGContextSaveGState(context)
CGContextAddRect(context, rect)
CGContextClip(context)
CGContextDrawLinearGradient(context, gradient, startPoint, endPoint, 0)
CGContextRestoreGState(context)
end
Final side note
The locations argument isn't even required in this case as CGGradientCreateWithColors() will automatically set the values to 0.0 and 1.0 for the first and last colour. Check the CGGradient Reference
locations
The location for each color provided in components. Each location must be a CGFloat value in the range of 0 to 1, inclusive. If 0 and 1 are not in the locations array, Quartz uses the colors provided that are closest to 0 and 1 for those locations.
If locations is NULL, the first color in colors is assigned to location 0, the last color incolors is assigned to location 1, and intervening colors are assigned locations that are at equal intervals in between.