swiftuigradientopacity

How to render a field strength in SwiftUI


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?


Solution

  • 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:

    #include <metal_stdlib>
    using namespace metal;
    
    [[ stitchable ]] half4 borderField(float2 position, half4 currentColor, float2 size, half4 newColor) {
        assert(size.x >= 0 && position.x <= size.x);
        assert(size.y >= 0 && position.y <= size.y);
    
        // Compute distance to border
        float dt = size.y - position.y;  // Distance to top
        float db = position.y;     // Distance to bottom
        float dl = position.x;      // Distance to left
        float dr = size.x - position.x;  // Distance to right
        float minDistance = min(min(dt, db), min(dl, dr));
        float r = minDistance + 1.0;
        float strength = 1.0 / sqrt(r);
        return half4(newColor.rgb, strength);
    
    }
    

    Add this to your project in a new .metal file.

    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 .colorEffect modifier:

    Rectangle()
        .frame(width: 300, height: 300)
        .colorEffect(ShaderLibrary.borderField(.float2(300, 300), .color(.blue)))
    

    Note that we're passing in the size and the base color here.

    This gives:

    enter image description here

    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:

    var body: some View {
        let start = Date.now
        Rectangle()
            .frame(width: boardSize.width, height: boardSize.height)
            .colorEffect(ShaderLibrary.borderField(.float2(boardSize.width, boardSize.height), .color(.green)))
        let _ = print(-start.timeIntervalSinceNow)
    }
    

    where boardSize has 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!