Skip to content

Instantly share code, notes, and snippets.

@eleev
Last active August 19, 2024 21:58
Show Gist options
  • Save eleev/3e3acc546ac814617a7710314abdc686 to your computer and use it in GitHub Desktop.
Save eleev/3e3acc546ac814617a7710314abdc686 to your computer and use it in GitHub Desktop.
Animated Voronoi Flow Gradient
//
// 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()
}
@eleev
Copy link
Author

eleev commented Aug 16, 2024

animated voronoi flow gradient

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:

  • Animation Speed: Control the speed of the animation.
  • Voronoi Cell Scale: Adjust the scale of the Voronoi cells.
  • Flow Directions: Modify the flow directions along the x and y axes.
  • Gradient Colors: Choose and customize the gradient colors.
  • Color Intensity Angles: Set angles to influence the intensity of colors.

@ivanopcode
Copy link

looks like os is dying trying to render this in swiftui..

@eleev
Copy link
Author

eleev commented Aug 16, 2024

@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