Last active
August 19, 2024 21:58
-
-
Save eleev/3e3acc546ac814617a7710314abdc686 to your computer and use it in GitHub Desktop.
Animated Voronoi Flow Gradient
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// VoronoiFlowGradient and the related sources | |
// Copyright (c) 2024 Astemir Eleev | |
// | |
// Permission is hereby granted, free of charge, to any person obtaining a copy | |
// of this software and associated documentation files (the "Software"), to deal | |
// in the Software without restriction, including without limitation the rights | |
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
// copies of the Software, and to permit persons to whom the Software is | |
// furnished to do so, subject to the following conditions: | |
// | |
// The above copyright notice and this permission notice shall be included in all | |
// copies or substantial portions of the Software. | |
// | |
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |
// SOFTWARE. | |
// | |
#include <metal_stdlib> | |
using namespace metal; | |
namespace VoronoiFlowGradient { | |
/** | |
* Generates a pseudo-random float2 based on the input float2 coordinates. | |
* | |
* @param uv Input coordinate as a float2. | |
* @return A pseudo-random float2 value. | |
*/ | |
float2 random(float2 uv) { | |
// Calculate dot products with specific constants to scramble the input coordinates | |
uv *= float2(dot(uv, float2(127.1, 311.7)), dot(uv, float2(227.1, 531.7))); | |
// Return a pseudo-random float2 using trigonometric transformations and fractal operations | |
return 1. - fract(tan(cos(uv) * 173.7) * 371915.3) * fract(tan(cos(uv) * 173.7) * 371915.3); | |
} | |
/** | |
* Computes a point in the Voronoi diagram adjusted by time. | |
* | |
* @param id The cell identifier as a float2. | |
* @param t The time variable to animate the points. | |
* @return A float2 representing the point's position. | |
*/ | |
float2 point(float2 id, float t) { | |
// Generate a point in the Voronoi cell using randomization and trigonometric functions | |
return sin(t * (random(id + .5) - .5) + random(id - 20.1) * 8.0) * .5; | |
} | |
/** | |
* Main function to generate a Voronoi flow gradient. | |
* | |
* This function is stitchable and designed to create a visually continuous Voronoi pattern. | |
* | |
* @param position The position coordinate as float2. | |
* @param bounds The bounds of the input space as float4 (minX, minY, maxX, maxY). | |
* @param time The current time used for animating the gradient. | |
* @param size The scaling factor for the Voronoi cells. | |
* @param offset The offset to apply over time, as float2. | |
* @param col1 The first color to blend in the gradient as float4 (R, G, B, A). | |
* @param col2 The second color to blend in the gradient as float4 (R, G, B, A). | |
* @param angle1 The first angle parameter for color blending. | |
* @param angle2 The second angle parameter for color blending. | |
* @return A half4 (RGBA) value representing the color at the given position. | |
*/ | |
[[ stitchable ]] half4 main( | |
float2 position, | |
float4 bounds, | |
float time, | |
float size, | |
float2 offset, | |
float4 col1, | |
float4 col2, | |
float angle1, | |
float angle2 | |
) { | |
// Normalize position based on bounds and adjust for center | |
float2 uv = (position - .5 * bounds.zw) / bounds.z; | |
uv.y = 1. - uv.y; // Flip Y coordinate to align with conventional top-down rendering | |
uv -= 0.2; // Offset to position the pattern | |
uv *= 0.6; // Scale down the pattern | |
// Apply animated offset and scale | |
float2 off = time / offset; | |
uv += off; | |
uv *= size; | |
// Get the fractional and integer parts of the UV coordinate | |
float2 gv = fract(uv) - .5; | |
float2 id = floor(uv); | |
float mindist = 1e9; // Initialize minimum distance as large value | |
float2 vorv; | |
// Loop over surrounding cells to find the closest point in the Voronoi diagram | |
for(float i = -2.; i <= 2.; i++) { | |
for(float j = -2.; j <= 2.; j++) { | |
float2 offv = float2(i, j); | |
float dist = length(gv + point(id + offv, time * 2.) - offv); | |
if (dist < mindist) { | |
mindist = dist; | |
vorv = (id + point(id + offv, time * 2.) + offv) / size - off; | |
} | |
} | |
} | |
// Blend between two colors based on calculated Voronoi point | |
half4 col = mix( | |
half4(col1.x, col1.y, col1.z, col1.a), | |
half4(col2.x, col2.y, col2.z, col1.a), | |
clamp(vorv.x * angle1 + vorv.y, angle2, 1.) * .5 + .5 | |
); | |
return col; | |
} | |
} | |
// MARK: SwiftUI API | |
/* | |
NOTE: You only need the following snippet, if you embed the shader and the modifier view into a package. | |
*/ | |
import SwiftUI | |
/// The Gradienta Metal shader library. | |
@available(iOS 17, macOS 14, macCatalyst 17, tvOS 17, visionOS 1, *) | |
@dynamicMemberLookup | |
public enum FrameworkShadersLibrary { | |
/// Returns a new shader function representing the stitchable MSL | |
/// function called `name` in the Inferno shader library. | |
/// | |
/// Typically this subscript is used implicitly via the dynamic | |
/// member syntax, for example: | |
/// | |
/// ```let fn = Framework.myFunction``` | |
/// | |
/// which creates a reference to the MSL function called | |
/// `myFunction()`. | |
public static subscript(dynamicMember name: String) -> ShaderFunction { | |
ShaderLibrary.bundle(.gradienta)[dynamicMember: name + "::main"] | |
} | |
} | |
extension Bundle { | |
public static var gradienta: Bundle = .module | |
} | |
public struct VoronoiFlowGradient: ShapeStyle, View, Sendable { | |
private var time: TimeInterval | |
private var size: CGFloat | |
private var offset: CGPoint | |
private var color1: Color | |
private var color2: Color | |
private var angle1: CGFloat | |
private var angle2: CGFloat | |
public init( | |
time: TimeInterval, | |
size: CGFloat = 10, | |
offset: CGPoint = CGPoint(x: -150, y: 300), | |
color1: Color = Color(red: 153 / 255, green: 41 / 255, blue: 196 / 255), | |
color2: Color = Color(red: 21 / 255, green: 241 / 255, blue: 211 / 255), | |
angle1: CGFloat = 2.1, | |
angle2: CGFloat = -1.3 | |
) { | |
self.time = time | |
self.size = size | |
self.offset = offset | |
self.color1 = color1 | |
self.color2 = color2 | |
self.angle1 = angle1 | |
self.angle2 = angle2 | |
} | |
public func resolve(in environment: EnvironmentValues) -> some ShapeStyle { | |
FrameworkShadersLibrary.VoronoiFlowGradient( | |
.boundingRect, | |
.float(time), | |
.float(size), | |
.float2(offset.x, offset.y), | |
.float4( | |
color1.components.red, | |
color1.components.green, | |
color1.components.blue, | |
color2.components.alpha | |
), | |
.float4( | |
color2.components.red, | |
color2.components.green, | |
color2.components.blue, | |
color2.components.alpha | |
), | |
.float(angle1), | |
.float(angle2) | |
) | |
} | |
} | |
/* | |
Utiilty Extension | |
*/ | |
extension Color { | |
var components: (red: CGFloat, green: CGFloat, blue: CGFloat, alpha: CGFloat) { | |
#if canImport(UIKit) | |
typealias NativeColor = UIColor | |
#elseif canImport(AppKit) | |
typealias NativeColor = NSColor | |
#endif | |
var r: CGFloat = 0 | |
var g: CGFloat = 0 | |
var b: CGFloat = 0 | |
var a: CGFloat = 0 | |
guard NativeColor(self).getRed(&r, green: &g, blue: &b, alpha: &a) else { | |
return (0, 0, 0, 0) | |
} | |
return (r, g, b, a) | |
} | |
} | |
/* | |
Usage: | |
*/ | |
struct ContentView: View { | |
private var start = Date() | |
var body: some View { | |
TimelineView(.animation) { context in | |
GeometryReader { proxy in | |
let time = context.date.timeIntervalSince1970 - start.timeIntervalSince1970 | |
VoronoiFlowGradient( | |
time: time * 0.5, | |
size: 15, | |
offset: CGPoint(x: 15, y: 15), | |
color1: .orange, | |
color2: .red, | |
angle1: 3.7, | |
angle2: -5.5 | |
) | |
} | |
} | |
.ignoresSafeArea() | |
} | |
} | |
#Preview { | |
ContentView() | |
} | |
looks like os is dying trying to render this in swiftui..
@ivanopcode did you measure it? If so, could you provide details on the methodology, including the OS version and device model used?
SwiftUI specifically has nothing to do with rendering, since it's almost fully delegated to Metal
OS technically also has nothing to do with "dying" (whatever it means) =]
The low frame rate of a .gif is due to its compression and size limitations. If your comment is based solely on this aspect, it is a bit of a misunderstanding and quite amusing. =]
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
The Animated Voronoi Flow Gradient is a Metal shader designed for dynamic visual effects. This shader is encapsulated as a shader modifier within a SwiftUI view, offering a range of customization options, including: