Skip to content

Commit bc67250

Browse files
committed
Integrate SwiftGlass for Vision theme
1 parent 6f94b7e commit bc67250

File tree

7 files changed

+413
-8
lines changed

7 files changed

+413
-8
lines changed
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
//
2+
// BlobShape.swift
3+
// SwiftGlass
4+
//
5+
// Created by Ming on 21/4/2025.
6+
//
7+
8+
import SwiftUI
9+
10+
@available(iOS 15.0, macOS 14.0, watchOS 10.0, tvOS 15.0, visionOS 1.0, *)
11+
public struct BlobGlassModifier: ViewModifier {
12+
// Parameters
13+
let color: Color
14+
let blobIntensity: CGFloat
15+
let animationSpeed: Double
16+
let complexity: Int
17+
let material: Material
18+
let gradientOpacity: Double
19+
let shadowOpacity: Double
20+
21+
// Animation state
22+
@State private var animationProgress: CGFloat = 0.0
23+
24+
// Initializer
25+
public init(
26+
color: Color,
27+
blobIntensity: CGFloat,
28+
animationSpeed: Double,
29+
complexity: Int,
30+
material: Material,
31+
gradientOpacity: Double,
32+
shadowOpacity: Double
33+
) {
34+
self.color = color
35+
self.blobIntensity = min(max(blobIntensity, 0.1), 1.0) // Constrain between 0.1 and 1.0
36+
self.animationSpeed = animationSpeed
37+
self.complexity = min(max(complexity, 4), 12) // Constrain between 4 and 12
38+
self.material = material
39+
self.gradientOpacity = gradientOpacity
40+
self.shadowOpacity = shadowOpacity
41+
}
42+
43+
// ViewModifier Body
44+
public func body(content: Content) -> some View {
45+
content
46+
// Clip content to animated blob shape
47+
.clipShape(
48+
BlobShape(
49+
complexity: complexity,
50+
animationProgress: animationProgress,
51+
intensity: blobIntensity
52+
)
53+
)
54+
// Blob glass background and effects
55+
.background(
56+
BlobShape(
57+
complexity: complexity,
58+
animationProgress: animationProgress,
59+
intensity: blobIntensity
60+
)
61+
.fill(material)
62+
.overlay(
63+
BlobShape(
64+
complexity: complexity,
65+
animationProgress: animationProgress,
66+
intensity: blobIntensity
67+
)
68+
.stroke(
69+
LinearGradient(
70+
colors: [
71+
color.opacity(gradientOpacity),
72+
color.opacity(0),
73+
color.opacity(gradientOpacity),
74+
color.opacity(0)
75+
],
76+
startPoint: .topLeading,
77+
endPoint: .bottomTrailing
78+
),
79+
lineWidth: 1.5
80+
)
81+
)
82+
.shadow(
83+
color: color.opacity(shadowOpacity),
84+
radius: 10,
85+
x: 0,
86+
y: 5
87+
)
88+
)
89+
.onAppear {
90+
// Start the continuous animation
91+
withAnimation(
92+
Animation
93+
.linear(duration: 10 / animationSpeed)
94+
.repeatForever(autoreverses: true)
95+
) {
96+
animationProgress = 1.0
97+
}
98+
}
99+
}
100+
}
101+
102+
// BlobShape
103+
@available(iOS 15.0, macOS 14.0, watchOS 10.0, tvOS 15.0, visionOS 1.0, *)
104+
public struct BlobShape: Shape {
105+
// Control points for the blob's perimeter
106+
var controlPoints: [UnitPoint]
107+
var animationProgress: CGFloat
108+
var intensity: CGFloat
109+
110+
// Initializer
111+
public init(complexity: Int = 8, animationProgress: CGFloat = 0.0, intensity: CGFloat = 0.5) {
112+
self.controlPoints = BlobShape.generateControlPoints(count: complexity)
113+
self.animationProgress = animationProgress
114+
self.intensity = intensity
115+
}
116+
117+
// Animatable
118+
public var animatableData: CGFloat {
119+
get { animationProgress }
120+
set { animationProgress = newValue }
121+
}
122+
123+
// Path Generation
124+
public func path(in rect: CGRect) -> Path {
125+
let center = CGPoint(x: rect.midX, y: rect.midY)
126+
let minDimension = min(rect.width, rect.height)
127+
let radius = minDimension / 2
128+
129+
// Calculate points on the blob's perimeter
130+
let points = controlPoints.map { unitPoint -> CGPoint in
131+
let angle = 2 * .pi * unitPoint.x + (animationProgress * 2 * .pi)
132+
let distortionAmount = sin(angle * 3 + animationProgress * 4) * intensity * 0.2
133+
let blobRadius = radius * (1 + distortionAmount)
134+
let pointX = center.x + cos(angle) * blobRadius
135+
let pointY = center.y + sin(angle) * blobRadius
136+
return CGPoint(x: pointX, y: pointY)
137+
}
138+
139+
// Use cubic Bézier curves for smoothness
140+
var path = Path()
141+
guard points.count > 2 else { return path }
142+
143+
path.move(to: points[0])
144+
145+
// Calculate control points for smooth cubic Bézier curves
146+
let count = points.count
147+
for i in 0..<count {
148+
let prev = points[(i - 1 + count) % count]
149+
let curr = points[i]
150+
let next = points[(i + 1) % count]
151+
let next2 = points[(i + 2) % count]
152+
153+
// Calculate control points for smoothness
154+
let control1 = CGPoint(
155+
x: curr.x + (next.x - prev.x) / 6,
156+
y: curr.y + (next.y - prev.y) / 6
157+
)
158+
let control2 = CGPoint(
159+
x: next.x - (next2.x - curr.x) / 6,
160+
y: next.y - (next2.y - curr.y) / 6
161+
)
162+
163+
path.addCurve(to: next, control1: control1, control2: control2)
164+
}
165+
166+
path.closeSubpath()
167+
return path
168+
}
169+
170+
// Helper: Generate evenly distributed control points around a circle
171+
private static func generateControlPoints(count: Int) -> [UnitPoint] {
172+
var points = [UnitPoint]()
173+
let angleStep = 1.0 / CGFloat(count)
174+
175+
for i in 0..<count {
176+
let angle = CGFloat(i) * angleStep
177+
points.append(UnitPoint(x: angle, y: angle))
178+
}
179+
180+
return points
181+
}
182+
}
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
//
2+
// GlassBackgroundModifier.swift
3+
// SwiftGlass
4+
//
5+
// Created by Ming on 21/4/2025.
6+
//
7+
8+
import SwiftUI
9+
10+
@available(iOS 15.0, macOS 14.0, watchOS 10.0, tvOS 15.0, visionOS 1.0, *)
11+
public struct GlassBackgroundModifier: ViewModifier {
12+
/// Controls when the glass effect should be displayed
13+
public enum GlassBackgroundDisplayMode {
14+
case always // Always show the effect
15+
case automatic // System determines visibility based on context
16+
}
17+
18+
/// Determines the gradient direction used in the glass effect's border
19+
public enum GradientStyle {
20+
case normal // Light at top-left and bottom-right
21+
case reverted // Light at top-right and bottom-left
22+
}
23+
24+
// Configuration properties for the glass effect
25+
let displayMode: GlassBackgroundDisplayMode
26+
let radius: CGFloat
27+
let color: Color
28+
let material: Material
29+
let gradientOpacity: Double
30+
let gradientStyle: GradientStyle
31+
let strokeWidth: CGFloat
32+
let shadowColor: Color
33+
let shadowOpacity: Double
34+
let shadowRadius: CGFloat
35+
let shadowX: CGFloat
36+
let shadowY: CGFloat
37+
38+
/// Creates a new glass effect modifier with the specified appearance settings
39+
public init(
40+
displayMode: GlassBackgroundDisplayMode,
41+
radius: CGFloat,
42+
color: Color,
43+
material: Material = .ultraThinMaterial,
44+
gradientOpacity: Double = 0.5,
45+
gradientStyle: GradientStyle = .normal,
46+
strokeWidth: CGFloat = 1.5,
47+
shadowColor: Color = .white,
48+
shadowOpacity: Double = 0.5,
49+
shadowRadius: CGFloat? = nil,
50+
shadowX: CGFloat = 0,
51+
shadowY: CGFloat = 5
52+
) {
53+
self.displayMode = displayMode
54+
self.radius = radius
55+
self.color = color
56+
self.material = material
57+
self.gradientOpacity = gradientOpacity
58+
self.gradientStyle = gradientStyle
59+
self.strokeWidth = strokeWidth
60+
self.shadowColor = shadowColor
61+
self.shadowOpacity = shadowOpacity
62+
self.shadowRadius = shadowRadius ?? radius
63+
self.shadowX = shadowX
64+
self.shadowY = shadowY
65+
}
66+
67+
/// Applies the glass effect to the content view
68+
/// Implementation uses three primary techniques:
69+
/// 1. Material background for the frosted effect
70+
/// 2. Gradient stroke for edge highlighting
71+
/// 3. Shadow for depth perception
72+
public func body(content: Content) -> some View {
73+
content
74+
.background(material) // Use the specified material for the frosted glass base
75+
.cornerRadius(radius) // Rounds the corners
76+
.overlay(
77+
// Adds subtle gradient border for dimensional effect
78+
RoundedRectangle(cornerRadius: radius)
79+
.stroke(
80+
LinearGradient(
81+
gradient: Gradient(colors: gradientColors()),
82+
startPoint: .topLeading,
83+
endPoint: .bottomTrailing
84+
),
85+
lineWidth: strokeWidth
86+
)
87+
)
88+
// Adds shadow for depth and elevation
89+
.shadow(color: shadowColor.opacity(shadowOpacity), radius: shadowRadius, x: shadowX, y: shadowY)
90+
}
91+
92+
/// Generates the gradient colors based on the selected style
93+
/// Creates the illusion of light reflection on glass edges
94+
private func gradientColors() -> [Color] {
95+
switch gradientStyle {
96+
case .normal:
97+
return [
98+
color.opacity(gradientOpacity),
99+
.clear,
100+
.clear,
101+
color.opacity(gradientOpacity)
102+
]
103+
case .reverted:
104+
return [
105+
.clear,
106+
color.opacity(gradientOpacity),
107+
color.opacity(gradientOpacity),
108+
.clear
109+
]
110+
}
111+
}
112+
}

0 commit comments

Comments
 (0)