Created
June 27, 2025 23:04
-
-
Save m1ckc3s/d221bcd875eeeb696b45d11551e60a7f to your computer and use it in GitHub Desktop.
This file contains hidden or 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
| // | |
| // DotPatternView.swift | |
| // x.com/mickces | |
| // | |
| // Created by mick on 4/27/25. | |
| // | |
| import SwiftUI | |
| public struct DotPatternView: View { | |
| public var dotSize: CGFloat | |
| public var spacing: CGFloat | |
| public var color: Color | |
| public var shimmerSpeed: Double | |
| public var dxFactor: Double | |
| public var dyFactor: Double | |
| public var baseAlpha: Double | |
| public var alphaMultiplier: Double | |
| public init( | |
| dotSize: CGFloat = 3, | |
| spacing: CGFloat = 18, | |
| color: Color = Color.gray.opacity(0.4), | |
| shimmerSpeed: Double = 2.0, | |
| dxFactor: Double = 0.25, | |
| dyFactor: Double = 0.21, | |
| baseAlpha: Double = 0.2, | |
| alphaMultiplier: Double = 3.0 | |
| ) { | |
| self.dotSize = dotSize | |
| self.spacing = spacing | |
| self.color = color | |
| self.shimmerSpeed = shimmerSpeed | |
| self.dxFactor = dxFactor | |
| self.dyFactor = dyFactor | |
| self.baseAlpha = baseAlpha | |
| self.alphaMultiplier = alphaMultiplier | |
| } | |
| public var body: some View { | |
| TimelineView(.animation) { context in | |
| shimmerCanvas(at: context.date.timeIntervalSinceReferenceDate) | |
| } | |
| } | |
| private func shimmerCanvas(at time: TimeInterval) -> some View { | |
| GeometryReader { geo in | |
| let cols = Int(geo.size.width / spacing) + 2 | |
| let rows = Int(geo.size.height / spacing) + 2 | |
| Canvas { ctx, _ in | |
| let dotRect = CGRect(origin: .zero, | |
| size: CGSize(width: dotSize, height: dotSize)) | |
| for row in 0...rows { | |
| for col in 0...cols { | |
| let x = CGFloat(col) * spacing | |
| let y = CGFloat(row) * spacing | |
| let hash = UInt32((row &* 73856093) ^ (col &* 19349663)) | |
| let phase = Double(hash & 0xFF) / 255.0 * .pi * 2 | |
| let freq = 0.7 + Double((hash >> 8) & 0xFF) / 255.0 | |
| let dx = Double(col) * dxFactor | |
| let dy = Double(row) * dyFactor | |
| let baseWave = sin(time * 0.6 + dx) + cos(time * 0.4 + dy) | |
| let pulse = sin(time * shimmerSpeed * freq + phase) | |
| let blended = (pulse + baseWave) / 4.0 | |
| let alpha = baseAlpha + alphaMultiplier * abs(blended) | |
| let dotColor = color.opacity(alpha) | |
| ctx.fill( | |
| Path(ellipseIn: dotRect.offsetBy(dx: x, dy: y)), | |
| with: .color(dotColor) | |
| ) | |
| } | |
| } | |
| } | |
| } | |
| .ignoresSafeArea() | |
| .allowsHitTesting(false) | |
| .accessibilityHidden(true) | |
| // .blur(radius: 0.25) // optional blur the dots | |
| .clipped() | |
| } | |
| } | |
| // MARK: - Preview | |
| #Preview { | |
| DotPatternDemoView() | |
| } | |
| struct DotPatternDemoView: View { | |
| @State private var colorOpacity: Double = 0.4 | |
| @State private var shimmerSpeed: Double = 2.0 | |
| @State private var dxFactor: Double = 0.25 | |
| @State private var dyFactor: Double = 0.21 | |
| @State private var baseAlpha: Double = 0.2 | |
| @State private var alphaMultiplier: Double = 3.0 | |
| var body: some View { | |
| ZStack { | |
| Color.black.ignoresSafeArea() | |
| DotPatternView( | |
| dotSize: 3, | |
| spacing: 18, | |
| color: Color.gray.opacity(colorOpacity), | |
| shimmerSpeed: shimmerSpeed, | |
| dxFactor: dxFactor, | |
| dyFactor: dyFactor, | |
| baseAlpha: baseAlpha, | |
| alphaMultiplier: alphaMultiplier | |
| ) | |
| .ignoresSafeArea() | |
| VStack { | |
| Spacer() | |
| VStack(spacing: 12) { | |
| HStack { | |
| Text(String(format: "%.2f", colorOpacity)) | |
| .monospacedDigit() | |
| Slider(value: $colorOpacity, in: 0.1...1) | |
| Text(String(format: "%.2f", colorOpacity)) | |
| .monospacedDigit() | |
| } | |
| HStack { | |
| Text(String(format: "%.2f", shimmerSpeed)) | |
| .monospacedDigit() | |
| Slider(value: $shimmerSpeed, in: 0.5...5) | |
| Text(String(format: "%.2f", shimmerSpeed)) | |
| .monospacedDigit() | |
| } | |
| HStack { | |
| Text(String(format: "%.2f", dxFactor)) | |
| .monospacedDigit() | |
| Slider(value: $dxFactor, in: 0.1...1, step: 0.05) | |
| Text(String(format: "%.2f", dxFactor)) | |
| .monospacedDigit() | |
| } | |
| HStack { | |
| Text(String(format: "%.2f", dyFactor)) | |
| .monospacedDigit() | |
| Slider(value: $dyFactor, in: 0.1...1, step: 0.05) | |
| Text(String(format: "%.2f", dyFactor)) | |
| .monospacedDigit() | |
| } | |
| HStack { | |
| Text(String(format: "%.2f", baseAlpha)) | |
| .monospacedDigit() | |
| Slider(value: $baseAlpha, in: 0.0...10.0, step: 0.1) | |
| Text(String(format: "%.2f", baseAlpha)) | |
| .monospacedDigit() | |
| } | |
| HStack { | |
| Text(String(format: "%.2f", alphaMultiplier)) | |
| .monospacedDigit() | |
| Slider(value: $alphaMultiplier, in: 0.0...10.0, step: 0.1) | |
| Text(String(format: "%.2f", alphaMultiplier)) | |
| .monospacedDigit() | |
| } | |
| } | |
| .padding() | |
| .background( | |
| ZStack { | |
| Color.black.opacity(0.6) | |
| } | |
| ) | |
| .cornerRadius(12) | |
| .padding() | |
| .foregroundColor(.white) | |
| .tint(.white) | |
| .controlSize(.small) | |
| .font(.system(size: 14, design: .monospaced)) | |
| } | |
| } | |
| } | |
| } |
Comments are disabled for this gist.