My app requires to render a field strength, i.e. the value of a function f(x,y) in a rectangle view.
I know that I can render linear or radial gradients, see here, but I did not find a way to set the brightness or opacity of a pixel of a View depending on a function f(x,y).
I could probably assemble the View of very many subviews where the brightness or opacity of each subview is set according to such a function, but this is ugly and there should be a better way.
Any suggestions?
How to render a field strength in SwiftUI
148 views Asked by Reinhard Männer AtThere are 3 answers
On
Here is my solution, based on the suggestion of lorem ipsum above.
This suggestion requires to compute an Image and to overlay it over a View.
I thus wrote the following extension to Image:
extension Image {
/// Creates an Image of a given size with a given color.
/// The color, without the alpha value, is here hard coded, but could of course be given by a parameter.
/// The alpha value represents the field function, depending on the (x,y) pixel value.
/// - Parameters:
/// - size: The size of the resulting image
/// - alpha: The field function
init?(size: CGSize, alpha: (_ x: Int, _ y: Int) -> Double) {
// Set color components as 8 bits in hex. Alpha will be taken from the function parameter.
// Pixel values are 32 bits: ARGB
let r: UInt32 = 0x00 << 16
let g: UInt32 = 0x00 << 8
let b: UInt32 = 0xFF
let pixelWidth = Int(size.width)
let pixelHeight = Int(size.height)
var srgbArray: [UInt32] = [] // The pixel array
// Compute field values
for y in 0 ..< pixelHeight {
for x in 0 ..< pixelWidth {
let fieldStrength = alpha(x, y)
if fieldStrength < 0 || fieldStrength > 1.0 {
return nil // The alpha function did return an illegal value (< 0 or > 1).
}
let a = UInt32(fieldStrength * 255.0) << 24 // alpha
srgbArray.append(a | r | g | b)
}
}
// https://forums.swift.org/t/creating-a-cgimage-from-color-array/18634
let cgImg = srgbArray.withUnsafeMutableBytes { ptr -> CGImage in
let ctx = CGContext(
data: ptr.baseAddress,
width: pixelWidth,
height: pixelHeight,
bitsPerComponent: 8,
bytesPerRow: 4 * pixelWidth,
space: CGColorSpace(name: CGColorSpace.sRGB)!,
bitmapInfo: CGBitmapInfo.byteOrder32Little.rawValue + CGImageAlphaInfo.premultipliedFirst.rawValue
)!
return ctx.makeImage()!
}
self.init(
cgImg,
scale: 1.0,
orientation: .up,
label: Text("Field")
)
}
}
The Image is initialized as
let image = Image(size: fieldSize, alpha: alpha(x:y:))
where function alpha can be any function. Here, for demo purposes, I am using the following function:
func alpha(x: Int, y: Int) -> Double {
Field.borderfield(size: fieldSize, x: x, y: y)
}
with
struct Field {
static func borderfield(size: CGSize, x: Int, y: Int) -> Double {
let a = Int(size.width)
let b = Int(size.height)
assert(x >= 0 && x <= a)
assert(y >= 0 && y <= b)
// Compute distance to border
let dt = b - y // Distance to top
let db = y // Distance to bottom
let dl = x // Distance to left
let dr = a - x // Distance to right
let minDistance = min(dt, db, dl, dr)
let d = Double(minDistance)
let r = d + 1.0 // min r is now 1
let signalStrength = 1.0/sqrt(r)
print(signalStrength)
return signalStrength
}
}
Function borderfield is a very rough approximation to the field that a charged border of the displayed rectangle would produce.
This is the image:

On
Here is a similar (but much shorter!) implementation using Canvas. I haven't looked at the performance but my experience with canvas is that it's extremely good, especially if you cache the resolved shading values as I have.
struct FieldStrengthView: View {
let color: Color
let alpha: (_ x: Int, _ y: Int) -> Double
var body: some View {
Canvas { context, size in
let pixelWidth = Int(size.width)
let pixelHeight = Int(size.height)
var shadings = [Double: GraphicsContext.Shading]()
for y in 0 ..< pixelHeight {
for x in 0 ..< pixelWidth {
let strength = alpha(x, y)
let path = Path(CGRect(x: x, y: y, width: 1, height: 1))
if let shading = shadings[strength] {
context.fill(path, with: shading)
} else {
let shading = GraphicsContext.Shading.color(color.opacity(strength))
shadings[strength] = shading
context.fill(path, with: shading)
}
}
}
}
}
}
It fills in one pixel at a time using the field strength from your existing function:
#Preview {
VStack {
FieldStrengthView(color: .blue, alpha: { x, y in
Field.borderfield(size: CGSize(width: 300, height: 300), x: x, y: y)
})
.frame(width: 300, height: 300)
FieldStrengthView(color: .green, alpha: { x, y in
Field.borderfield(size: CGSize(width: 200, height: 200), x: x, y: y)
})
.frame(width: 200, height: 200)
}
}
Gives:

Here is an alternative answer using a Metal shader. You'd need to create a shader for each implementation of the field strength algorithm, here's my attempt at your border field:
Add this to your project in a new
.metalfile.The first two parameters are passed by default, which is the position of the current pixel and its current colour. The subsequent ones are up to you.
To use in SwiftUI, use the
.colorEffectmodifier:Note that we're passing in the size and the base color here.
This gives:
I'd be fascinated to know the performance differences you encounter between these implementations.
——
Update from Reinhard Männer where it seems Metal really is quite a lot faster!
Your 1st solution was already great, but this one is absolutely great!
I implemented it in my app.
I am not sure how to time it right, but I did the following:
where
boardSizehas been set the same as in my previous timing (900,357).Earlier, the rendering took about 0.15 sec. With your metal solution and my timing above, the result is
3.0040740966796875e-05
If my timing is right, this is an unbelievable speedup of 5.000. And this motivates me, to look also into metal. Thanks a lot!