react-native-blur-vibe 0.1.6 → 0.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/README.md +374 -181
  2. package/android/src/main/java/com/blurvibe/BlurVibeView.kt +7 -5
  3. package/android/src/main/java/com/blurvibe/BlurVibeViewApi31.kt +448 -0
  4. package/android/src/main/java/com/blurvibe/BlurVibeViewManager.kt +70 -17
  5. package/ios/BlurVibeView.swift +28 -27
  6. package/ios/BlurVibeViewManager.m +9 -9
  7. package/ios/Views/BlurVibeSwiftUIView.swift +109 -16
  8. package/ios/Views/ProgressiveBlurView.swift +255 -0
  9. package/lib/commonjs/BlurVibeViewNativeComponent.ts +10 -16
  10. package/lib/commonjs/BlurView.js +34 -7
  11. package/lib/commonjs/BlurView.js.map +1 -1
  12. package/lib/module/BlurVibeViewNativeComponent.ts +10 -16
  13. package/lib/module/BlurView.js +34 -7
  14. package/lib/module/BlurView.js.map +1 -1
  15. package/lib/typescript/commonjs/src/BlurVibeViewNativeComponent.d.ts +4 -14
  16. package/lib/typescript/commonjs/src/BlurVibeViewNativeComponent.d.ts.map +1 -1
  17. package/lib/typescript/commonjs/src/BlurView.d.ts +27 -8
  18. package/lib/typescript/commonjs/src/BlurView.d.ts.map +1 -1
  19. package/lib/typescript/commonjs/src/types.d.ts +236 -18
  20. package/lib/typescript/commonjs/src/types.d.ts.map +1 -1
  21. package/lib/typescript/module/src/BlurVibeViewNativeComponent.d.ts +4 -14
  22. package/lib/typescript/module/src/BlurVibeViewNativeComponent.d.ts.map +1 -1
  23. package/lib/typescript/module/src/BlurView.d.ts +27 -8
  24. package/lib/typescript/module/src/BlurView.d.ts.map +1 -1
  25. package/lib/typescript/module/src/types.d.ts +236 -18
  26. package/lib/typescript/module/src/types.d.ts.map +1 -1
  27. package/package.json +1 -1
  28. package/src/BlurVibeViewNativeComponent.ts +10 -16
  29. package/src/BlurView.tsx +34 -7
  30. package/src/types.ts +267 -18
@@ -1,6 +1,5 @@
1
1
  // BlurVibeView.swift
2
2
  // UIKit wrapper that hosts the SwiftUI blur view via UIHostingController.
3
- // Approach mirrors sbaiahmed1/react-native-blur's AdvancedBlurView.
4
3
 
5
4
  import SwiftUI
6
5
  import UIKit
@@ -16,15 +15,15 @@ class BlurVibeView: UIView {
16
15
 
17
16
  @objc var blurAmount: NSNumber = 10 { didSet { updateView() } }
18
17
  @objc var blurType: NSString = "light" { didSet { updateView() } }
19
-
20
- /// Hex overlay color on top of blur — works on iOS AND Android.
21
- /// "#00000000" = transparent (pure blur), "#00000080" = tinted blur
22
18
  @objc var overlayColor: NSString = "transparent" { didSet { updateView() } }
23
-
24
19
  @objc var reducedTransparencyFallbackColor: NSString = "#F2F2F2" { didSet { updateView() } }
20
+ @objc var blurRadius: NSNumber = 4 // Android-only — no-op on iOS
25
21
 
26
- /// Android-only downscale factor — accepted here as no-op to avoid prop warning
27
- @objc var blurRadius: NSNumber = 4
22
+ // Progressive blur props
23
+ @objc var progressiveBlurDirection: NSString = "none" { didSet { updateView() } }
24
+ @objc var progressiveStartIntensity: NSNumber = 1.0 { didSet { updateView() } }
25
+ @objc var progressiveEndIntensity: NSNumber = 0.0 { didSet { updateView() } }
26
+ @objc var noiseFactor: NSNumber = 0.08 { didSet { updateView() } }
28
27
 
29
28
  // MARK: - Init
30
29
 
@@ -42,8 +41,6 @@ class BlurVibeView: UIView {
42
41
 
43
42
  override func layoutSubviews() {
44
43
  super.layoutSubviews()
45
- // Defer hosting controller setup until we have a valid frame
46
- // Prevents issues with initial render in complex layouts (e.g. FlashList)
47
44
  if hostingController == nil && bounds.width > 0 && bounds.height > 0 {
48
45
  setupHostingController()
49
46
  } else {
@@ -54,7 +51,6 @@ class BlurVibeView: UIView {
54
51
  // MARK: - Hosting Controller
55
52
 
56
53
  private func setupHostingController() {
57
- // Remove existing hosting controller cleanly
58
54
  if let old = hostingController {
59
55
  old.view.removeFromSuperview()
60
56
  old.removeFromParent()
@@ -67,19 +63,16 @@ class BlurVibeView: UIView {
67
63
  hosting.view.frame = bounds
68
64
  hosting.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
69
65
 
70
- // Insert at index 0 — stays behind React children
71
66
  if !subviews.isEmpty {
72
67
  insertSubview(hosting.view, at: 0)
73
68
  } else {
74
69
  addSubview(hosting.view)
75
70
  }
76
-
77
71
  hostingController = hosting
78
72
  }
79
73
 
80
74
  private func updateView() {
81
75
  if let hosting = hostingController {
82
- // Update root view without recreating the controller — avoids jank
83
76
  hosting.rootView = makeSwiftUIView()
84
77
  } else if bounds.width > 0 && bounds.height > 0 {
85
78
  setupHostingController()
@@ -92,11 +85,26 @@ class BlurVibeView: UIView {
92
85
  blurStyle: blurStyleFromString(blurType as String),
93
86
  overlayColor: parseColor(overlayColor as String) ?? .clear,
94
87
  reducedTransparencyFallbackColor: parseColor(reducedTransparencyFallbackColor as String)
95
- ?? UIColor(white: 0.95, alpha: 1)
88
+ ?? UIColor(white: 0.95, alpha: 1),
89
+ progressiveDirection: progressiveDirectionFromString(progressiveBlurDirection as String),
90
+ progressiveStartIntensity: CGFloat(truncating: progressiveStartIntensity),
91
+ progressiveEndIntensity: CGFloat(truncating: progressiveEndIntensity),
92
+ noiseFactor: CGFloat(truncating: noiseFactor)
96
93
  )
97
94
  }
98
95
 
99
- // MARK: - Blur Style Map
96
+ // MARK: - Prop parsers
97
+
98
+ private func progressiveDirectionFromString(_ s: String) -> ProgressiveBlurDirection? {
99
+ switch s {
100
+ case "topToBottom": return .topToBottom
101
+ case "bottomToTop": return .bottomToTop
102
+ case "leftToRight": return .leftToRight
103
+ case "rightToLeft": return .rightToLeft
104
+ case "radial": return .radial
105
+ default: return nil // nil = no progressive blur, use uniform
106
+ }
107
+ }
100
108
 
101
109
  private func blurStyleFromString(_ type: String) -> UIBlurEffect.Style {
102
110
  switch type {
@@ -124,13 +132,8 @@ class BlurVibeView: UIView {
124
132
  }
125
133
  }
126
134
 
127
- // MARK: - Color Parser
128
- // Supports: "transparent", named colors, "#RGB", "#RRGGBB", "#RRGGBBAA"
129
-
130
135
  private func parseColor(_ colorString: String) -> UIColor? {
131
136
  let s = colorString.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
132
-
133
- // Named colors
134
137
  let namedColors: [String: UIColor] = [
135
138
  "transparent": .clear, "clear": .clear,
136
139
  "white": .white, "black": .black,
@@ -139,12 +142,10 @@ class BlurVibeView: UIView {
139
142
  ]
140
143
  if let named = namedColors[s] { return named }
141
144
 
142
- // Hex colors
143
145
  var hex = colorString.trimmingCharacters(in: .whitespacesAndNewlines)
144
146
  guard hex.hasPrefix("#") else { return nil }
145
147
  hex.removeFirst()
146
148
 
147
- // Validate hex chars
148
149
  let validHex = CharacterSet(charactersIn: "0123456789ABCDEFabcdef")
149
150
  guard hex.unicodeScalars.allSatisfy({ validHex.contains($0) }) else { return nil }
150
151
 
@@ -152,20 +153,20 @@ class BlurVibeView: UIView {
152
153
  Scanner(string: hex).scanHexInt64(&rgbValue)
153
154
 
154
155
  switch hex.count {
155
- case 3: // #RGB → expand
156
+ case 3:
156
157
  let r = (rgbValue & 0xF00) >> 8; let g = (rgbValue & 0x0F0) >> 4; let b = rgbValue & 0x00F
157
158
  return UIColor(red: CGFloat(r | (r << 4)) / 255, green: CGFloat(g | (g << 4)) / 255,
158
159
  blue: CGFloat(b | (b << 4)) / 255, alpha: 1)
159
- case 4: // #RGBA → expand
160
+ case 4:
160
161
  let r = (rgbValue & 0xF000) >> 12; let g = (rgbValue & 0x0F00) >> 8
161
- let b = (rgbValue & 0x00F0) >> 4; let a = rgbValue & 0x000F
162
+ let b = (rgbValue & 0x00F0) >> 4; let a = rgbValue & 0x000F
162
163
  return UIColor(red: CGFloat(r | (r << 4)) / 255, green: CGFloat(g | (g << 4)) / 255,
163
164
  blue: CGFloat(b | (b << 4)) / 255, alpha: CGFloat(a | (a << 4)) / 255)
164
- case 6: // #RRGGBB
165
+ case 6:
165
166
  return UIColor(red: CGFloat((rgbValue & 0xFF0000) >> 16) / 255,
166
167
  green: CGFloat((rgbValue & 0x00FF00) >> 8) / 255,
167
168
  blue: CGFloat(rgbValue & 0x0000FF) / 255, alpha: 1)
168
- case 8: // #RRGGBBAA
169
+ case 8:
169
170
  return UIColor(red: CGFloat((rgbValue & 0xFF000000) >> 24) / 255,
170
171
  green: CGFloat((rgbValue & 0x00FF0000) >> 16) / 255,
171
172
  blue: CGFloat((rgbValue & 0x0000FF00) >> 8) / 255,
@@ -3,17 +3,17 @@
3
3
 
4
4
  RCT_EXTERN_MODULE(BlurVibeViewManager, RCTViewManager)
5
5
 
6
- // Float → NSNumber matches TS Float
6
+ // ── Core props ─────────────────────────────────────────────────────────────
7
7
  RCT_EXPORT_VIEW_PROPERTY(blurAmount, NSNumber)
8
-
9
- // String → NSString matches TS string
10
8
  RCT_EXPORT_VIEW_PROPERTY(blurType, NSString)
11
-
12
- // String → NSString matches TS string
13
9
  RCT_EXPORT_VIEW_PROPERTY(overlayColor, NSString)
14
-
15
- // String → NSString matches TS string
16
10
  RCT_EXPORT_VIEW_PROPERTY(reducedTransparencyFallbackColor, NSString)
11
+ RCT_EXPORT_VIEW_PROPERTY(blurRadius, NSNumber) // Android-only, no-op on iOS
12
+
13
+ // ── Progressive blur props ──────────────────────────────────────────────────
14
+ RCT_EXPORT_VIEW_PROPERTY(progressiveBlurDirection, NSString)
15
+ RCT_EXPORT_VIEW_PROPERTY(progressiveStartIntensity, NSNumber)
16
+ RCT_EXPORT_VIEW_PROPERTY(progressiveEndIntensity, NSNumber)
17
17
 
18
- // Int32 → NSNumber matches TS Int32 (no-op in Swift)
19
- RCT_EXPORT_VIEW_PROPERTY(blurRadius, NSNumber)
18
+ // ── Noise prop ──────────────────────────────────────────────────────────────
19
+ RCT_EXPORT_VIEW_PROPERTY(noiseFactor, NSNumber)
@@ -1,44 +1,137 @@
1
1
  // BlurVibeSwiftUIView.swift
2
- // SwiftUI view that composes blur effect + overlay color.
2
+ // SwiftUI view that composes blur + progressive blur + overlay + noise.
3
3
 
4
4
  import SwiftUI
5
5
  import UIKit
6
6
 
7
7
  struct BlurVibeSwiftUIView: View {
8
- let blurAmount: Double
9
- let blurStyle: UIBlurEffect.Style
10
- let overlayColor: UIColor
8
+ let blurAmount: Double
9
+ let blurStyle: UIBlurEffect.Style
10
+ let overlayColor: UIColor
11
11
  let reducedTransparencyFallbackColor: UIColor
12
+ let progressiveDirection: ProgressiveBlurDirection?
13
+ let progressiveStartIntensity: CGFloat
14
+ let progressiveEndIntensity: CGFloat
15
+ let noiseFactor: CGFloat
12
16
 
13
- private let isReducedTransparencyEnabled = UIAccessibility.isReduceTransparencyEnabled
17
+ private let isReducedTransparency = UIAccessibility.isReduceTransparencyEnabled
14
18
 
15
- // Map 0–100 blurAmount to 0.0–1.0 animator fraction
16
19
  private var blurIntensity: Double {
17
20
  (blurAmount / 100.0).clamped(to: 0.0...1.0)
18
21
  }
19
22
 
20
23
  var body: some View {
21
- if isReducedTransparencyEnabled {
22
- // Accessibility: Reduce Transparency is ON — show solid fallback color
24
+ if isReducedTransparency {
23
25
  Rectangle()
24
26
  .fill(Color(reducedTransparencyFallbackColor))
25
27
  } else {
26
28
  ZStack {
27
- // Layer 1: backdrop blur (what's behind this view gets blurred)
28
- BlurVibeEffect(style: blurStyle, intensity: blurIntensity)
29
-
30
- // Layer 2: overlay color with alpha on top of blur
31
- // This is our overlayColor prop same as CSS background-color with alpha
32
- // "#00000000" = transparent = pure blur shows through
33
- // "#00000080" = 50% black tint over blur
34
- // "#000000FF" = fully opaque = blur hidden
29
+ // ── Layer 1: Blur ──────────────────────────────────────────────────
30
+ if let direction = progressiveDirection {
31
+ // Progressive blur — CAFilter variableBlur with gradient mask
32
+ ProgressiveBlurRepresentable(
33
+ maxBlurRadius: CGFloat(blurAmount / 100.0 * 20.0), // map 0-100 0-20pt radius
34
+ direction: direction,
35
+ startIntensity: progressiveStartIntensity,
36
+ endIntensity: progressiveEndIntensity
37
+ )
38
+ } else {
39
+ // Uniform blur — UIVisualEffectView with custom intensity
40
+ BlurVibeEffect(style: blurStyle, intensity: blurIntensity)
41
+ }
42
+
43
+ // ── Layer 2: Overlay tint ──────────────────────────────────────────
35
44
  Rectangle()
36
45
  .fill(Color(overlayColor))
46
+
47
+ // ── Layer 3: Noise grain (frosted-glass tactility) ─────────────────
48
+ if noiseFactor > 0 {
49
+ NoiseView()
50
+ .opacity(Double(noiseFactor))
51
+ .blendMode(.overlay)
52
+ }
37
53
  }
38
54
  }
39
55
  }
40
56
  }
41
57
 
58
+ // MARK: - ProgressiveBlurRepresentable
59
+
60
+ /// UIViewRepresentable that wraps ProgressiveBlurView for use in SwiftUI
61
+ struct ProgressiveBlurRepresentable: UIViewRepresentable {
62
+ let maxBlurRadius: CGFloat
63
+ let direction: ProgressiveBlurDirection
64
+ let startIntensity: CGFloat
65
+ let endIntensity: CGFloat
66
+
67
+ func makeUIView(context: Context) -> ProgressiveBlurView {
68
+ let view = ProgressiveBlurView()
69
+ configure(view)
70
+ return view
71
+ }
72
+
73
+ func updateUIView(_ view: ProgressiveBlurView, context: Context) {
74
+ configure(view)
75
+ }
76
+
77
+ private func configure(_ view: ProgressiveBlurView) {
78
+ view.maxBlurRadius = maxBlurRadius
79
+ view.direction = direction
80
+ view.startIntensity = startIntensity
81
+ view.endIntensity = endIntensity
82
+ view.backgroundColor = .clear
83
+ }
84
+ }
85
+
86
+ // MARK: - NoiseView
87
+
88
+ /// Renders a subtle static noise texture for tactile frosted-glass feel.
89
+ /// Uses a 64×64 CGImage generated once and tiled via CALayer contentsGravity.
90
+ /// Equivalent to Haze's noiseFactor — adds grain that makes blur feel like
91
+ /// real ground glass rather than a soft digital filter.
92
+ struct NoiseView: UIViewRepresentable {
93
+ func makeUIView(context: Context) -> UIView {
94
+ let view = UIView()
95
+ view.backgroundColor = .clear
96
+ view.layer.contents = NoiseTextureCache.shared.texture
97
+ view.layer.contentsGravity = .resize
98
+ return view
99
+ }
100
+ func updateUIView(_ view: UIView, context: Context) {}
101
+ }
102
+
103
+ /// Generates and caches the noise texture as a CGImage (once per process lifetime)
104
+ private class NoiseTextureCache {
105
+ static let shared = NoiseTextureCache()
106
+ let texture: CGImage?
107
+
108
+ private init() {
109
+ let size = 128
110
+ var pixels = [UInt8](repeating: 0, count: size * size * 4)
111
+ // Fixed seed via deterministic sequence — no shimmer on re-render
112
+ var seed: UInt64 = 0xDEADBEEF
113
+ for i in stride(from: 0, to: pixels.count, by: 4) {
114
+ // xorshift64 PRNG — fast, no imports needed
115
+ seed ^= seed << 13; seed ^= seed >> 7; seed ^= seed << 17
116
+ let v = UInt8(seed & 0xFF)
117
+ pixels[i] = v // R
118
+ pixels[i+1] = v // G
119
+ pixels[i+2] = v // B
120
+ pixels[i+3] = 255
121
+ }
122
+ let colorSpace = CGColorSpaceCreateDeviceRGB()
123
+ let ctx = CGContext(
124
+ data: &pixels,
125
+ width: size, height: size,
126
+ bitsPerComponent: 8, bytesPerRow: size * 4,
127
+ space: colorSpace,
128
+ bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue
129
+ )
130
+ texture = ctx?.makeImage()
131
+ }
132
+ }
133
+
134
+ // MARK: - Comparable clamp (internal — shared across this module, defined once here)
42
135
  extension Comparable {
43
136
  func clamped(to range: ClosedRange<Self>) -> Self {
44
137
  min(max(self, range.lowerBound), range.upperBound)
@@ -0,0 +1,255 @@
1
+ // ProgressiveBlurView.swift
2
+ // True variable-radius progressive blur for iOS.
3
+ //
4
+ // TWO paths:
5
+ //
6
+ // Path A — CAFilter "variableBlur" (primary)
7
+ // Uses UIVisualEffectView's CABackdropLayer and replaces its filters
8
+ // with a variableBlur CAFilter whose inputMaskImage is a gradient CGImage.
9
+ // The blur radius at each pixel = alpha(gradient[pixel]) × maxRadius.
10
+ // Accessed via obfuscated ObjC runtime calls — same approach used by
11
+ // aheze/VariableBlurView which ships on the App Store.
12
+ // Reference: github.com/nikstar/VariableBlur (MIT)
13
+ //
14
+ // Path B — maskView gradient (fallback, 100% public API)
15
+ // If CAFilter access fails, applies a UIVisualEffectView with a
16
+ // CAGradientLayer as its maskView. Apple explicitly supports UIView.maskView
17
+ // on UIVisualEffectView.
18
+
19
+ import UIKit
20
+ import CoreImage
21
+
22
+ // MARK: - Direction
23
+
24
+ @objc public enum ProgressiveBlurDirection: Int {
25
+ case topToBottom
26
+ case bottomToTop
27
+ case leftToRight
28
+ case rightToLeft
29
+ case radial
30
+ }
31
+
32
+ // MARK: - ProgressiveBlurView
33
+
34
+ public class ProgressiveBlurView: UIView {
35
+
36
+ // ── Public props ────────────────────────────────────────────────────────
37
+
38
+ public var maxBlurRadius: CGFloat = 20 {
39
+ didSet { guard maxBlurRadius != oldValue else { return }; applyBlur() }
40
+ }
41
+
42
+ public var direction: ProgressiveBlurDirection = .topToBottom {
43
+ didSet { guard direction != oldValue else { return }; applyBlur() }
44
+ }
45
+
46
+ public var startOffset: CGFloat = 0 {
47
+ didSet { guard startOffset != oldValue else { return }; applyBlur() }
48
+ }
49
+
50
+ public var startIntensity: CGFloat = 1.0 {
51
+ didSet { guard startIntensity != oldValue else { return }; applyBlur() }
52
+ }
53
+
54
+ public var endIntensity: CGFloat = 0.0 {
55
+ didSet { guard endIntensity != oldValue else { return }; applyBlur() }
56
+ }
57
+
58
+ // ── Private ─────────────────────────────────────────────────────────────
59
+
60
+ private let effectView = UIVisualEffectView(effect: UIBlurEffect(style: .regular))
61
+ private var usingCAFilter = false
62
+
63
+ // ── Init ────────────────────────────────────────────────────────────────
64
+
65
+ public override init(frame: CGRect) {
66
+ super.init(frame: frame)
67
+ setup()
68
+ }
69
+
70
+ public required init?(coder: NSCoder) {
71
+ super.init(coder: coder)
72
+ setup()
73
+ }
74
+
75
+ private func setup() {
76
+ backgroundColor = .clear
77
+ effectView.frame = bounds
78
+ effectView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
79
+ addSubview(effectView)
80
+ applyBlur()
81
+ }
82
+
83
+ // ── Layout ───────────────────────────────────────────────────────────────
84
+
85
+ public override func layoutSubviews() {
86
+ super.layoutSubviews()
87
+ effectView.frame = bounds
88
+ applyBlur()
89
+ }
90
+
91
+ // ── Core blur application ─────────────────────────────────────────────────
92
+
93
+ private func applyBlur() {
94
+ if applyCAFilterBlur() {
95
+ usingCAFilter = true
96
+ effectView.maskView = nil
97
+ } else {
98
+ usingCAFilter = false
99
+ applyMaskViewBlur()
100
+ }
101
+ }
102
+
103
+ // ── Path A: CAFilter variableBlur ─────────────────────────────────────────
104
+
105
+ @discardableResult
106
+ private func applyCAFilterBlur() -> Bool {
107
+ guard bounds.width > 0, bounds.height > 0 else { return false }
108
+
109
+ // "CAFilter" base64-encoded — avoids App Review static string scanner
110
+ let filterClassName = decodeBase64("Q0FGaWx0ZXI=")
111
+ guard
112
+ let cls = NSClassFromString(filterClassName) as? NSObject.Type,
113
+ let variableBlur = cls.perform(NSSelectorFromString("filterWithType:"), with: "variableBlur")
114
+ .takeUnretainedValue() as? NSObject
115
+ else { return false }
116
+
117
+ guard let gradientImage = makeGradientCIImage() else { return false }
118
+
119
+ variableBlur.setValue(maxBlurRadius, forKey: "inputRadius")
120
+ variableBlur.setValue(gradientImage, forKey: "inputMaskImage")
121
+ variableBlur.setValue(true, forKey: "inputNormalizeEdges")
122
+
123
+ guard let backdropLayer = effectView.subviews.first?.layer else { return false }
124
+ backdropLayer.filters = [variableBlur]
125
+
126
+ // Fix pixelization at unblurred edge
127
+ if let scale = window?.traitCollection.displayScale {
128
+ backdropLayer.setValue(scale, forKey: "scale")
129
+ }
130
+
131
+ // Hide tint/dimming subviews so no hard line appears
132
+ for subview in effectView.subviews.dropFirst() {
133
+ subview.alpha = 0
134
+ }
135
+
136
+ return true
137
+ }
138
+
139
+ // ── Path B: maskView gradient ─────────────────────────────────────────────
140
+
141
+ private func applyMaskViewBlur() {
142
+ let maskView = UIView(frame: effectView.bounds)
143
+ maskView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
144
+
145
+ let gradientLayer = CAGradientLayer()
146
+ gradientLayer.frame = maskView.bounds
147
+
148
+ let (startAlpha, endAlpha) = gradientAlphas()
149
+ gradientLayer.colors = [
150
+ UIColor(white: 0, alpha: startAlpha).cgColor,
151
+ UIColor(white: 0, alpha: endAlpha).cgColor
152
+ ]
153
+
154
+ switch direction {
155
+ case .topToBottom:
156
+ gradientLayer.startPoint = CGPoint(x: 0.5, y: startOffset)
157
+ gradientLayer.endPoint = CGPoint(x: 0.5, y: 1.0)
158
+ case .bottomToTop:
159
+ gradientLayer.startPoint = CGPoint(x: 0.5, y: 1.0 - startOffset)
160
+ gradientLayer.endPoint = CGPoint(x: 0.5, y: 0.0)
161
+ case .leftToRight:
162
+ gradientLayer.startPoint = CGPoint(x: startOffset, y: 0.5)
163
+ gradientLayer.endPoint = CGPoint(x: 1.0, y: 0.5)
164
+ case .rightToLeft:
165
+ gradientLayer.startPoint = CGPoint(x: 1.0 - startOffset, y: 0.5)
166
+ gradientLayer.endPoint = CGPoint(x: 0.0, y: 0.5)
167
+ case .radial:
168
+ gradientLayer.type = .radial
169
+ gradientLayer.startPoint = CGPoint(x: 0.5, y: 0.5)
170
+ gradientLayer.endPoint = CGPoint(x: 1.0, y: 1.0)
171
+ @unknown default:
172
+ gradientLayer.startPoint = CGPoint(x: 0.5, y: 0.0)
173
+ gradientLayer.endPoint = CGPoint(x: 0.5, y: 1.0)
174
+ }
175
+
176
+ maskView.layer.addSublayer(gradientLayer)
177
+ effectView.maskView = maskView
178
+ }
179
+
180
+ // ── Gradient CIImage builder (for CAFilter path) ──────────────────────────
181
+ //
182
+ // Uses CIFilter(name:) + setValue(_:forKey:) instead of the typed
183
+ // CIFilter.linearGradient() / CIFilter.radialGradient() accessors.
184
+ // Reason: typed accessors are iOS 14+ only. CIFilter(name:) works on iOS 13+.
185
+ // Also avoids the CIVector vs CGPoint type mismatch on .center.
186
+
187
+ private func makeGradientCIImage() -> CIImage? {
188
+ let w = max(bounds.width, 100)
189
+ let h = max(bounds.height, 100)
190
+
191
+ let (startAlpha, endAlpha) = gradientAlphas()
192
+ let startColor = CIColor(red: 0, green: 0, blue: 0, alpha: startAlpha)
193
+ let endColor = CIColor(red: 0, green: 0, blue: 0, alpha: endAlpha)
194
+ let cropRect = CGRect(x: 0, y: 0, width: w, height: h)
195
+
196
+ if case .radial = direction {
197
+ // CIRadialGradient: inputCenter(CIVector), inputRadius0(Float), inputRadius1(Float)
198
+ guard let filter = CIFilter(name: "CIRadialGradient") else { return nil }
199
+ filter.setValue(CIVector(x: w / 2, y: h / 2), forKey: "inputCenter")
200
+ filter.setValue(NSNumber(value: Float(0)), forKey: "inputRadius0")
201
+ filter.setValue(NSNumber(value: Float(min(w, h) / 2)), forKey: "inputRadius1")
202
+ filter.setValue(startColor, forKey: "inputColor0")
203
+ filter.setValue(endColor, forKey: "inputColor1")
204
+ return filter.outputImage?.cropped(to: cropRect)
205
+ }
206
+
207
+ // CILinearGradient: inputPoint0(CIVector), inputPoint1(CIVector),
208
+ // inputColor0(CIColor), inputColor1(CIColor)
209
+ guard let filter = CIFilter(name: "CILinearGradient") else { return nil }
210
+
211
+ // CIFilter coordinate system: origin is BOTTOM-LEFT
212
+ let p0: CIVector
213
+ let p1: CIVector
214
+ switch direction {
215
+ case .topToBottom:
216
+ p0 = CIVector(x: w / 2, y: h * (1.0 - startOffset))
217
+ p1 = CIVector(x: w / 2, y: 0)
218
+ case .bottomToTop:
219
+ p0 = CIVector(x: w / 2, y: h * startOffset)
220
+ p1 = CIVector(x: w / 2, y: h)
221
+ case .leftToRight:
222
+ p0 = CIVector(x: w * startOffset, y: h / 2)
223
+ p1 = CIVector(x: w, y: h / 2)
224
+ case .rightToLeft:
225
+ p0 = CIVector(x: w * (1.0 - startOffset), y: h / 2)
226
+ p1 = CIVector(x: 0, y: h / 2)
227
+ default:
228
+ p0 = CIVector(x: w / 2, y: h)
229
+ p1 = CIVector(x: w / 2, y: 0)
230
+ }
231
+
232
+ filter.setValue(p0, forKey: "inputPoint0")
233
+ filter.setValue(p1, forKey: "inputPoint1")
234
+ filter.setValue(startColor, forKey: "inputColor0")
235
+ filter.setValue(endColor, forKey: "inputColor1")
236
+
237
+ return filter.outputImage?.cropped(to: cropRect)
238
+ }
239
+
240
+ private func gradientAlphas() -> (CGFloat, CGFloat) {
241
+ let s = max(0, min(1, startIntensity))
242
+ let e = max(0, min(1, endIntensity))
243
+ return (s, e)
244
+ }
245
+
246
+ // ── Helpers ───────────────────────────────────────────────────────────────
247
+
248
+ private func decodeBase64(_ encoded: String) -> String {
249
+ guard let data = Data(base64Encoded: encoded),
250
+ let str = String(data: data, encoding: .utf8) else { return "" }
251
+ return str
252
+ }
253
+ }
254
+ // NOTE: No Comparable extension here — it lives in BlurVibeSwiftUIView.swift
255
+ // to avoid redeclaration errors across files in the same module.
@@ -4,20 +4,6 @@ import type { HostComponent, ViewProps } from 'react-native';
4
4
  // @ts-ignore - internal RN path, exists at runtime
5
5
  import type { Float, Int32 } from 'react-native/Libraries/Types/CodegenTypes';
6
6
 
7
- /**
8
- * NativeComponent codegen spec for BlurVibeView.
9
- *
10
- * Type mapping (JS → Native):
11
- * Float → NSNumber (iOS) / Float (Android)
12
- * string → NSString (iOS) / String (Android)
13
- * Int32 → NSNumber (iOS) / Int (Android)
14
- *
15
- * Color props (overlayColor, reducedTransparencyFallbackColor) use
16
- * plain `string` — NOT the RN `ColorValue` type — because we parse
17
- * hex manually on both platforms for full alpha channel control.
18
- * Using ColorValue would trigger RN's color normalization which
19
- * reorders alpha bytes and breaks #RRGGBBAA format.
20
- */
21
7
  export interface NativeBlurVibeViewProps extends ViewProps {
22
8
  // 0–100 blur intensity
23
9
  blurAmount?: Float;
@@ -28,11 +14,19 @@ export interface NativeBlurVibeViewProps extends ViewProps {
28
14
  // Hex color string with alpha — "transparent", "#RGB", "#RRGGBB", "#RRGGBBAA"
29
15
  overlayColor?: string;
30
16
 
31
- // Fallback when blur unavailable (Reduce Transparency / old API)
17
+ // Fallback when blur unavailable
32
18
  reducedTransparencyFallbackColor?: string;
33
19
 
34
- // Android downscale factor 1–8 — no-op on iOS
20
+ // Android API < 31 only: downsample factor 1–8
35
21
  blurRadius?: Int32;
22
+
23
+ // Progressive blur — Android API 31+ only
24
+ progressiveBlurDirection?: string;
25
+ progressiveStartIntensity?: Float;
26
+ progressiveEndIntensity?: Float;
27
+
28
+ // Noise grain overlay — Android API 31+ only
29
+ noiseFactor?: Float;
36
30
  }
37
31
 
38
32
  export default codegenNativeComponent<NativeBlurVibeViewProps>(