-
-
Save winstondu/fc821ac142617b0521957511518c2eff 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)) | |
| } | |
| } | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment