react-native-blur-vibe 0.1.13 → 0.1.14
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.
package/README.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# React Native Blur-Vibe
|
|
2
2
|
|
|
3
|
-
<img width="
|
|
3
|
+
<a href="https://www.npmjs.com/package/react-native-blur-vibe"><img width="100%" height="35%" alt="github-banner" src="https://github.com/user-attachments/assets/78b2e5ec-5b57-48c0-b984-69cb57cbcf26" /></a>
|
|
4
4
|
<br></br>
|
|
5
5
|
|
|
6
|
-
A modern, actively maintained blur view for React Native. Works on **iOS** and **Android** with
|
|
6
|
+
A modern, actively maintained blur view for React Native. Works on **iOS** and **Android** with both Old (Paper) and New (Fabric) Architecture support.
|
|
7
7
|
|
|
8
8
|
> The key difference from other blur libraries: `overlayColor` works on **both iOS and Android** — letting you control blur visibility the same way CSS `backdrop-filter` + `background-color` works on the web.
|
|
9
9
|
|
|
@@ -7,6 +7,7 @@ import android.graphics.Canvas
|
|
|
7
7
|
import android.graphics.Color
|
|
8
8
|
import android.graphics.LinearGradient
|
|
9
9
|
import android.graphics.Paint
|
|
10
|
+
import android.graphics.PixelFormat
|
|
10
11
|
import android.graphics.PorterDuff
|
|
11
12
|
import android.graphics.PorterDuffXfermode
|
|
12
13
|
import android.graphics.RadialGradient
|
|
@@ -17,12 +18,10 @@ import android.os.Build
|
|
|
17
18
|
import android.os.Handler
|
|
18
19
|
import android.os.HandlerThread
|
|
19
20
|
import android.os.Looper
|
|
20
|
-
import android.renderscript.Allocation
|
|
21
|
-
import android.renderscript.Element
|
|
22
|
-
import android.renderscript.RenderScript
|
|
23
|
-
import android.renderscript.ScriptIntrinsicBlur
|
|
24
21
|
import android.util.TypedValue
|
|
25
22
|
import android.view.Choreographer
|
|
23
|
+
import android.view.PixelCopy
|
|
24
|
+
import android.view.Surface
|
|
26
25
|
import android.view.View
|
|
27
26
|
import android.view.ViewGroup
|
|
28
27
|
import android.view.ViewOutlineProvider
|
|
@@ -57,48 +56,40 @@ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
|
|
|
57
56
|
private var noiseBitmap: Bitmap? = null
|
|
58
57
|
private val noisePaint = Paint()
|
|
59
58
|
|
|
60
|
-
// ──
|
|
59
|
+
// ── Bitmap double-buffer ──────────────────────────────────────────────────
|
|
60
|
+
//
|
|
61
|
+
// pixelCopyBitmap: written by PixelCopy (on its own callback thread)
|
|
62
|
+
// scaledBitmap: written by workerThread after downsampling
|
|
63
|
+
// readyBitmap: @Volatile — RenderThread reads this in onDraw()
|
|
61
64
|
|
|
62
|
-
private var
|
|
63
|
-
private var scaledBitmap:
|
|
64
|
-
@Volatile private var readyBitmap:
|
|
65
|
+
private var pixelCopyBitmap: Bitmap? = null
|
|
66
|
+
private var scaledBitmap: Bitmap? = null
|
|
67
|
+
@Volatile private var readyBitmap: Bitmap? = null
|
|
65
68
|
|
|
66
69
|
private val capturePaint = Paint(Paint.FILTER_BITMAP_FLAG)
|
|
67
70
|
private val bitmapPaint = Paint(Paint.FILTER_BITMAP_FLAG or Paint.ANTI_ALIAS_FLAG)
|
|
68
71
|
|
|
69
|
-
// ── Worker thread
|
|
72
|
+
// ── Worker thread ─────────────────────────────────────────────────────────
|
|
70
73
|
|
|
71
74
|
private val workerThread = HandlerThread("BlurVibeWorker31-${hashCode()}")
|
|
72
75
|
.also { it.start() }
|
|
73
76
|
private val workerHandler = Handler(workerThread.looper)
|
|
74
77
|
private val mainHandler = Handler(Looper.getMainLooper())
|
|
75
78
|
|
|
76
|
-
// ──
|
|
77
|
-
|
|
78
|
-
@Suppress("DEPRECATION")
|
|
79
|
-
private var rs: RenderScript? = null
|
|
80
|
-
@Suppress("DEPRECATION")
|
|
81
|
-
private var blurScript: ScriptIntrinsicBlur? = null
|
|
82
|
-
@Suppress("DEPRECATION")
|
|
83
|
-
private var inAlloc: Allocation? = null
|
|
84
|
-
@Suppress("DEPRECATION")
|
|
85
|
-
private var outAlloc: Allocation? = null
|
|
79
|
+
// ── Root / window ─────────────────────────────────────────────────────────
|
|
86
80
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
private
|
|
90
|
-
private val myLoc = IntArray(2)
|
|
91
|
-
private val rootLoc = IntArray(2)
|
|
81
|
+
private var blurRoot: ViewGroup? = null
|
|
82
|
+
private val myLoc = IntArray(2)
|
|
83
|
+
private val rootLoc = IntArray(2)
|
|
92
84
|
|
|
93
85
|
// ── State ─────────────────────────────────────────────────────────────────
|
|
94
86
|
|
|
95
|
-
// isCapturing: suppresses our own draw() during root.draw() capture
|
|
96
|
-
// so we don't paint stale blur into the capture bitmap (static blur bug)
|
|
97
87
|
var isCapturing = false
|
|
98
88
|
private set
|
|
99
89
|
private var blurEnabled = true
|
|
100
90
|
private var autoUpdate = true
|
|
101
91
|
private var frameScheduled = false
|
|
92
|
+
private var pixelCopyInFlight = false
|
|
102
93
|
|
|
103
94
|
// ── Choreographer gate ────────────────────────────────────────────────────
|
|
104
95
|
|
|
@@ -126,13 +117,8 @@ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
|
|
|
126
117
|
|
|
127
118
|
init {
|
|
128
119
|
setWillNotDraw(false)
|
|
129
|
-
// DO NOT call setBackgroundColor — it replaces ReactViewGroup's
|
|
130
|
-
// ReactViewBackgroundDrawable, killing all RN style prop handling.
|
|
131
|
-
//
|
|
132
120
|
// outlineProvider = BACKGROUND: ReactViewBackgroundDrawable implements
|
|
133
|
-
// getOutline() for all RN borderRadius variants.
|
|
134
|
-
// by default — only enabled when a non-zero radius is actually set,
|
|
135
|
-
// to avoid GPU clip stack issues with overflow:hidden + Reanimated.
|
|
121
|
+
// getOutline() correctly for all RN borderRadius variants automatically.
|
|
136
122
|
outlineProvider = ViewOutlineProvider.BACKGROUND
|
|
137
123
|
}
|
|
138
124
|
|
|
@@ -143,22 +129,19 @@ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
|
|
|
143
129
|
blurRoot = findBlurRoot()
|
|
144
130
|
safeAddPreDrawListener()
|
|
145
131
|
generateNoiseBitmap()
|
|
146
|
-
workerHandler.post { initRenderScript() }
|
|
147
132
|
scheduleFrame()
|
|
148
133
|
}
|
|
149
134
|
|
|
150
135
|
override fun onDetachedFromWindow() {
|
|
151
136
|
safeRemovePreDrawListener()
|
|
152
137
|
Choreographer.getInstance().removeFrameCallback(frameCallback)
|
|
153
|
-
frameScheduled
|
|
154
|
-
isCapturing
|
|
155
|
-
|
|
156
|
-
|
|
138
|
+
frameScheduled = false
|
|
139
|
+
isCapturing = false
|
|
140
|
+
pixelCopyInFlight = false
|
|
141
|
+
blurRoot = null
|
|
142
|
+
readyBitmap = null
|
|
157
143
|
noiseBitmap?.recycle(); noiseBitmap = null
|
|
158
|
-
workerHandler.post {
|
|
159
|
-
releaseBitmapPool()
|
|
160
|
-
releaseRenderScript()
|
|
161
|
-
}
|
|
144
|
+
workerHandler.post { releaseBitmapPool() }
|
|
162
145
|
super.onDetachedFromWindow()
|
|
163
146
|
}
|
|
164
147
|
|
|
@@ -180,10 +163,6 @@ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
|
|
|
180
163
|
}
|
|
181
164
|
|
|
182
165
|
// ── draw() — no-op during root capture ────────────────────────────────────
|
|
183
|
-
//
|
|
184
|
-
// Prevents stale blur output from being captured into the background bitmap.
|
|
185
|
-
// When isCapturing=true, root.draw() is in progress — we skip ourselves
|
|
186
|
-
// so only the content BEHIND us is captured.
|
|
187
166
|
|
|
188
167
|
override fun draw(canvas: Canvas) {
|
|
189
168
|
if (isCapturing) return
|
|
@@ -198,12 +177,13 @@ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
|
|
|
198
177
|
val h = height.toFloat(); if (h <= 0f) return
|
|
199
178
|
|
|
200
179
|
val bmp = readyBitmap?.takeIf { !it.isRecycled } ?: run {
|
|
201
|
-
// No blur ready yet —
|
|
180
|
+
// No blur ready yet — show overlay color as placeholder
|
|
202
181
|
if (Color.alpha(overlayColor) > 0) {
|
|
203
182
|
overlayPaint.color = overlayColor
|
|
204
183
|
canvas.drawRect(0f, 0f, w, h, overlayPaint)
|
|
205
184
|
}
|
|
206
|
-
|
|
185
|
+
// Redraw border on top even when no blur ready
|
|
186
|
+
background?.draw(canvas)
|
|
207
187
|
return
|
|
208
188
|
}
|
|
209
189
|
|
|
@@ -212,7 +192,7 @@ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
|
|
|
212
192
|
canvas.saveLayer(0f, 0f, w, h, null)
|
|
213
193
|
} else -1
|
|
214
194
|
|
|
215
|
-
// Step 2:
|
|
195
|
+
// Step 2: blurred bitmap
|
|
216
196
|
canvas.drawBitmap(bmp, null, RectF(0f, 0f, w, h), bitmapPaint)
|
|
217
197
|
|
|
218
198
|
// Step 3: progressive alpha mask
|
|
@@ -236,108 +216,250 @@ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
|
|
|
236
216
|
noisePaint.shader = BitmapShader(noise, Shader.TileMode.REPEAT, Shader.TileMode.REPEAT)
|
|
237
217
|
canvas.drawRect(0f, 0f, w, h, noisePaint)
|
|
238
218
|
}
|
|
239
|
-
|
|
240
|
-
// Step 6: let ReactViewGroup draw borders/radius on top
|
|
241
|
-
super.onDraw(canvas)
|
|
219
|
+
background?.draw(canvas)
|
|
242
220
|
}
|
|
243
221
|
|
|
244
|
-
// ── Capture
|
|
222
|
+
// ── Capture pipeline — PixelCopy (API 31+) ────────────────────────────────
|
|
245
223
|
|
|
246
224
|
private fun captureAndBlur() {
|
|
247
|
-
if (isCapturing) return
|
|
248
|
-
val root
|
|
249
|
-
val
|
|
250
|
-
val
|
|
251
|
-
|
|
252
|
-
|
|
225
|
+
if (isCapturing || pixelCopyInFlight) return
|
|
226
|
+
val root = blurRoot ?: return
|
|
227
|
+
val vw = width; if (vw <= 0) return
|
|
228
|
+
val vh = height; if (vh <= 0) return
|
|
229
|
+
|
|
230
|
+
// Compute this view's screen rect for PixelCopy
|
|
231
|
+
getLocationInWindow(myLoc)
|
|
232
|
+
root.getLocationInWindow(rootLoc)
|
|
233
|
+
|
|
234
|
+
// Screen-space rect of the CONTENT BEHIND this view (use root location
|
|
235
|
+
// as origin since PixelCopy works in window coordinates)
|
|
236
|
+
val srcRect = Rect(
|
|
237
|
+
myLoc[0], myLoc[1],
|
|
238
|
+
myLoc[0] + vw, myLoc[1] + vh
|
|
239
|
+
)
|
|
253
240
|
|
|
254
241
|
val sw = (vw / DOWNSAMPLE).toInt().coerceAtLeast(1)
|
|
255
242
|
val sh = (vh / DOWNSAMPLE).toInt().coerceAtLeast(1)
|
|
256
243
|
|
|
257
|
-
|
|
244
|
+
val destBitmap = reuseBitmap(pixelCopyBitmap, vw, vh)
|
|
245
|
+
.also { pixelCopyBitmap = it }
|
|
246
|
+
|
|
247
|
+
// Hide ourselves during PixelCopy so we capture ONLY content behind us
|
|
248
|
+
isCapturing = true
|
|
249
|
+
pixelCopyInFlight = true
|
|
250
|
+
|
|
251
|
+
val window = (context as? android.app.Activity)?.window
|
|
252
|
+
?: run {
|
|
253
|
+
// Fallback to root.draw() if window not available
|
|
254
|
+
isCapturing = false
|
|
255
|
+
pixelCopyInFlight = false
|
|
256
|
+
captureWithRootDraw()
|
|
257
|
+
return
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
PixelCopy.request(
|
|
261
|
+
window,
|
|
262
|
+
srcRect,
|
|
263
|
+
destBitmap,
|
|
264
|
+
{ result ->
|
|
265
|
+
isCapturing = false
|
|
266
|
+
pixelCopyInFlight = false
|
|
267
|
+
|
|
268
|
+
if (result != PixelCopy.SUCCESS) {
|
|
269
|
+
// PixelCopy failed — fall back to root.draw()
|
|
270
|
+
mainHandler.post { captureWithRootDraw() }
|
|
271
|
+
return@request
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Blur on worker thread
|
|
275
|
+
val captureRef = destBitmap
|
|
276
|
+
workerHandler.post {
|
|
277
|
+
val scaled = reuseBitmap(scaledBitmap, sw, sh).also { scaledBitmap = it }
|
|
278
|
+
|
|
279
|
+
// Downsample
|
|
280
|
+
Canvas(scaled).drawBitmap(
|
|
281
|
+
captureRef,
|
|
282
|
+
Rect(0, 0, captureRef.width, captureRef.height),
|
|
283
|
+
Rect(0, 0, scaled.width, scaled.height),
|
|
284
|
+
capturePaint
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
// Multi-pass software Gaussian blur
|
|
288
|
+
val radius = blurRadiusFromAmount(blurAmount)
|
|
289
|
+
repeat(BLUR_ROUNDS) { stackBlur(scaled, radius.toInt().coerceAtLeast(1)) }
|
|
290
|
+
|
|
291
|
+
readyBitmap = scaled
|
|
292
|
+
mainHandler.post { invalidate() }
|
|
293
|
+
}
|
|
294
|
+
},
|
|
295
|
+
mainHandler
|
|
296
|
+
)
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// ── Fallback: root.draw() when PixelCopy unavailable ─────────────────────
|
|
300
|
+
|
|
301
|
+
private fun captureWithRootDraw() {
|
|
302
|
+
if (isCapturing) return
|
|
303
|
+
val root = blurRoot ?: return
|
|
304
|
+
val vw = width; if (vw <= 0) return
|
|
305
|
+
val vh = height; if (vh <= 0) return
|
|
306
|
+
val sw = (vw / DOWNSAMPLE).toInt().coerceAtLeast(1)
|
|
307
|
+
val sh = (vh / DOWNSAMPLE).toInt().coerceAtLeast(1)
|
|
308
|
+
|
|
258
309
|
root.getLocationInWindow(rootLoc)
|
|
259
310
|
getLocationInWindow(myLoc)
|
|
260
311
|
val offsetX = (myLoc[0] - rootLoc[0]).toFloat()
|
|
261
312
|
val offsetY = (myLoc[1] - rootLoc[1]).toFloat()
|
|
262
313
|
|
|
263
|
-
val capture = reuseBitmap(
|
|
264
|
-
val scaled = reuseBitmap(scaledBitmap,
|
|
314
|
+
val capture = reuseBitmap(pixelCopyBitmap, vw, vh).also { pixelCopyBitmap = it }
|
|
315
|
+
val scaled = reuseBitmap(scaledBitmap, sw, sh).also { scaledBitmap = it }
|
|
265
316
|
|
|
266
|
-
// Capture — isCapturing suppresses our own draw() so root.draw() skips us
|
|
267
317
|
isCapturing = true
|
|
268
318
|
val c = Canvas(capture)
|
|
269
319
|
c.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR)
|
|
270
320
|
c.translate(-offsetX, -offsetY)
|
|
271
|
-
try {
|
|
272
|
-
root.draw(c)
|
|
273
|
-
} catch (_: Exception) {
|
|
274
|
-
isCapturing = false
|
|
275
|
-
return
|
|
276
|
-
}
|
|
321
|
+
try { root.draw(c) } catch (_: Exception) { isCapturing = false; return }
|
|
277
322
|
isCapturing = false
|
|
278
323
|
|
|
279
|
-
// Downsample + blur on worker thread (never blocks main/RenderThread)
|
|
280
324
|
val captureRef = capture
|
|
281
|
-
val scaledRef = scaled
|
|
282
|
-
val radius = blurRadiusFromAmount(blurAmount)
|
|
283
|
-
|
|
284
325
|
workerHandler.post {
|
|
285
|
-
|
|
286
|
-
Canvas(scaledRef).drawBitmap(
|
|
326
|
+
Canvas(scaled).drawBitmap(
|
|
287
327
|
captureRef,
|
|
288
328
|
Rect(0, 0, captureRef.width, captureRef.height),
|
|
289
|
-
Rect(0, 0,
|
|
329
|
+
Rect(0, 0, scaled.width, scaled.height),
|
|
290
330
|
capturePaint
|
|
291
331
|
)
|
|
292
|
-
|
|
293
|
-
repeat(BLUR_ROUNDS) {
|
|
294
|
-
|
|
295
|
-
// Atomic swap: readyBitmap is @Volatile — RenderThread sees new value immediately
|
|
296
|
-
// We never mutate scaledRef after this point until the next capture starts
|
|
297
|
-
readyBitmap = scaledRef
|
|
298
|
-
|
|
332
|
+
val radius = blurRadiusFromAmount(blurAmount)
|
|
333
|
+
repeat(BLUR_ROUNDS) { stackBlur(scaled, radius.toInt().coerceAtLeast(1)) }
|
|
334
|
+
readyBitmap = scaled
|
|
299
335
|
mainHandler.post { invalidate() }
|
|
300
336
|
}
|
|
301
337
|
}
|
|
302
338
|
|
|
303
|
-
// ──
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
339
|
+
// ── Stack blur (pure Kotlin, no deprecated APIs) ──────────────────────────
|
|
340
|
+
//
|
|
341
|
+
// Mario Klingemann's StackBlur — O(w×h) regardless of radius.
|
|
342
|
+
// Fast, no RenderScript, works on all API levels, zero deprecation warnings.
|
|
343
|
+
// Used by many production apps including Facebook's Fresco library.
|
|
344
|
+
// radius clamped 1–254 (algorithm limit).
|
|
345
|
+
|
|
346
|
+
private fun stackBlur(bmp: Bitmap, radius: Int) {
|
|
347
|
+
val r = radius.coerceIn(1, 254)
|
|
348
|
+
val w = bmp.width
|
|
349
|
+
val h = bmp.height
|
|
350
|
+
val pixels = IntArray(w * h)
|
|
351
|
+
bmp.getPixels(pixels, 0, w, 0, 0, w, h)
|
|
352
|
+
|
|
353
|
+
val div = r + r + 1
|
|
354
|
+
val wm = w - 1
|
|
355
|
+
val hm = h - 1
|
|
356
|
+
val wh = w * h
|
|
357
|
+
val divSum = (div + 1) shr 1
|
|
358
|
+
val divSumSq = divSum * divSum
|
|
359
|
+
val dv = IntArray(256 * divSumSq) { it / divSumSq }
|
|
360
|
+
|
|
361
|
+
var yi = 0
|
|
362
|
+
val vmin = IntArray(maxOf(w, h))
|
|
363
|
+
val vmax = IntArray(maxOf(w, h))
|
|
364
|
+
|
|
365
|
+
val rStack = IntArray(div)
|
|
366
|
+
val gStack = IntArray(div)
|
|
367
|
+
val bStack = IntArray(div)
|
|
368
|
+
|
|
369
|
+
for (y in 0 until h) {
|
|
370
|
+
var rSum = 0; var gSum = 0; var bSum = 0
|
|
371
|
+
var rOut = 0; var gOut = 0; var bOut = 0
|
|
372
|
+
|
|
373
|
+
var p = pixels[yi]
|
|
374
|
+
var pr = (p shr 16) and 0xFF
|
|
375
|
+
var pg = (p shr 8) and 0xFF
|
|
376
|
+
var pb = p and 0xFF
|
|
377
|
+
|
|
378
|
+
for (i in 0 until divSum) {
|
|
379
|
+
rStack[i] = pr; gStack[i] = pg; bStack[i] = pb
|
|
380
|
+
rSum += pr * (i + 1); gSum += pg * (i + 1); bSum += pb * (i + 1)
|
|
381
|
+
rOut += pr; gOut += pg; bOut += pb
|
|
382
|
+
}
|
|
383
|
+
for (i in 1 until divSum) {
|
|
384
|
+
val ii = if (i <= wm) i else wm
|
|
385
|
+
p = pixels[yi + ii]
|
|
386
|
+
pr = (p shr 16) and 0xFF; pg = (p shr 8) and 0xFF; pb = p and 0xFF
|
|
387
|
+
rStack[i + r] = pr; gStack[i + r] = pg; bStack[i + r] = pb
|
|
388
|
+
rSum += pr * (divSum - i)
|
|
389
|
+
gSum += pg * (divSum - i)
|
|
390
|
+
bSum += pb * (divSum - i)
|
|
391
|
+
}
|
|
327
392
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
393
|
+
var si = r
|
|
394
|
+
for (x in 0 until w) {
|
|
395
|
+
pixels[yi + x] = (-0x1000000 or (dv[rSum] shl 16) or (dv[gSum] shl 8) or dv[bSum])
|
|
396
|
+
rSum -= rOut; gSum -= gOut; bSum -= bOut
|
|
397
|
+
rOut -= rStack[si]; gOut -= gStack[si]; bOut -= bStack[si]
|
|
398
|
+
var sip = si + divSum
|
|
399
|
+
if (sip >= div) sip -= div
|
|
400
|
+
pr = rStack[sip]; pg = gStack[sip]; pb = bStack[sip]
|
|
401
|
+
rOut += pr; gOut += pg; bOut += pb
|
|
402
|
+
rSum += rOut; gSum += gOut; bSum += bOut
|
|
403
|
+
if (x < r) vmin[x] = x + r + 1 else if (x + r < wm) vmin[x] = x + r + 1 else vmin[x] = wm
|
|
404
|
+
if (x > r) vmax[x] = x - r else vmax[x] = 0
|
|
405
|
+
val sp = pixels[yi + vmin[x]]
|
|
406
|
+
val vp = pixels[yi + vmax[x]]
|
|
407
|
+
rStack[sip] = (sp shr 16) and 0xFF
|
|
408
|
+
gStack[sip] = (sp shr 8) and 0xFF
|
|
409
|
+
bStack[sip] = sp and 0xFF
|
|
410
|
+
rOut += rStack[sip] - ((vp shr 16) and 0xFF)
|
|
411
|
+
gOut += gStack[sip] - ((vp shr 8) and 0xFF)
|
|
412
|
+
bOut += bStack[sip] - (vp and 0xFF)
|
|
413
|
+
if (++si >= div) si = 0
|
|
414
|
+
}
|
|
415
|
+
yi += w
|
|
331
416
|
}
|
|
332
|
-
Canvas(bmp).drawBitmap(bmp, 0f, 0f, p)
|
|
333
|
-
}
|
|
334
417
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
418
|
+
var xi = 0
|
|
419
|
+
for (x in 0 until w) {
|
|
420
|
+
var rSum = 0; var gSum = 0; var bSum = 0
|
|
421
|
+
var rOut = 0; var gOut = 0; var bOut = 0
|
|
422
|
+
var yp = -r * w
|
|
423
|
+
var p = pixels[xi]
|
|
424
|
+
var pr = (p shr 16) and 0xFF
|
|
425
|
+
var pg = (p shr 8) and 0xFF
|
|
426
|
+
var pb = p and 0xFF
|
|
427
|
+
for (i in 0 until divSum) {
|
|
428
|
+
rStack[i] = pr; gStack[i] = pg; bStack[i] = pb
|
|
429
|
+
rSum += pr * (i + 1); gSum += pg * (i + 1); bSum += pb * (i + 1)
|
|
430
|
+
rOut += pr; gOut += pg; bOut += pb
|
|
431
|
+
}
|
|
432
|
+
for (i in 1..r) {
|
|
433
|
+
if (i <= hm) yp += w
|
|
434
|
+
p = pixels[xi + yp]
|
|
435
|
+
pr = (p shr 16) and 0xFF; pg = (p shr 8) and 0xFF; pb = p and 0xFF
|
|
436
|
+
rStack[i + r] = pr; gStack[i + r] = pg; bStack[i + r] = pb
|
|
437
|
+
rSum += pr * (divSum - i); gSum += pg * (divSum - i); bSum += pb * (divSum - i)
|
|
438
|
+
}
|
|
439
|
+
var si = r
|
|
440
|
+
for (y in 0 until h) {
|
|
441
|
+
pixels[xi + y * w] = (-0x1000000 or (dv[rSum] shl 16) or (dv[gSum] shl 8) or dv[bSum])
|
|
442
|
+
rSum -= rOut; gSum -= gOut; bSum -= bOut
|
|
443
|
+
rOut -= rStack[si]; gOut -= gStack[si]; bOut -= bStack[si]
|
|
444
|
+
var sip = si + divSum; if (sip >= div) sip -= div
|
|
445
|
+
pr = rStack[sip]; pg = gStack[sip]; pb = bStack[sip]
|
|
446
|
+
rOut += pr; gOut += pg; bOut += pb
|
|
447
|
+
rSum += rOut; gSum += gOut; bSum += bOut
|
|
448
|
+
vmin[y] = if (y + r < hm) (y + r + 1) * w else hm * w
|
|
449
|
+
vmax[y] = if (y > r) (y - r) * w else 0
|
|
450
|
+
val sp = pixels[xi + vmin[y]]
|
|
451
|
+
val vp = pixels[xi + vmax[y]]
|
|
452
|
+
rStack[sip] = (sp shr 16) and 0xFF
|
|
453
|
+
gStack[sip] = (sp shr 8) and 0xFF
|
|
454
|
+
bStack[sip] = sp and 0xFF
|
|
455
|
+
rOut += rStack[sip] - ((vp shr 16) and 0xFF)
|
|
456
|
+
gOut += gStack[sip] - ((vp shr 8) and 0xFF)
|
|
457
|
+
bOut += bStack[sip] - (vp and 0xFF)
|
|
458
|
+
if (++si >= div) si = 0
|
|
459
|
+
}
|
|
460
|
+
xi++
|
|
461
|
+
}
|
|
462
|
+
bmp.setPixels(pixels, 0, w, 0, 0, w, h)
|
|
341
463
|
}
|
|
342
464
|
|
|
343
465
|
// ── Progressive shader ────────────────────────────────────────────────────
|
|
@@ -383,9 +505,6 @@ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
|
|
|
383
505
|
cornerRadiusPx = TypedValue.applyDimension(
|
|
384
506
|
TypedValue.COMPLEX_UNIT_DIP, radiusDp, context.resources.displayMetrics
|
|
385
507
|
)
|
|
386
|
-
// Only enable clipToOutline when radius > 0.
|
|
387
|
-
// Keeping it false when not needed avoids GPU clip stack issues
|
|
388
|
-
// when overflow:hidden is set on parent + Reanimated is animating.
|
|
389
508
|
clipToOutline = cornerRadiusPx > 0f
|
|
390
509
|
invalidate()
|
|
391
510
|
}
|
|
@@ -413,9 +532,7 @@ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
|
|
|
413
532
|
else {
|
|
414
533
|
safeRemovePreDrawListener()
|
|
415
534
|
Choreographer.getInstance().removeFrameCallback(frameCallback)
|
|
416
|
-
frameScheduled = false
|
|
417
|
-
readyBitmap = null
|
|
418
|
-
invalidate()
|
|
535
|
+
frameScheduled = false; readyBitmap = null; invalidate()
|
|
419
536
|
}
|
|
420
537
|
}
|
|
421
538
|
|
|
@@ -459,11 +576,10 @@ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
|
|
|
459
576
|
}
|
|
460
577
|
|
|
461
578
|
private fun releaseBitmapPool() {
|
|
462
|
-
|
|
463
|
-
scaledBitmap?.recycle();
|
|
579
|
+
pixelCopyBitmap?.recycle(); pixelCopyBitmap = null
|
|
580
|
+
scaledBitmap?.recycle(); scaledBitmap = null
|
|
464
581
|
}
|
|
465
582
|
|
|
466
|
-
@Suppress("DEPRECATION")
|
|
467
583
|
private fun reuseBitmap(existing: Bitmap?, w: Int, h: Int): Bitmap {
|
|
468
584
|
if (existing != null && !existing.isRecycled
|
|
469
585
|
&& existing.width == w && existing.height == h) return existing
|
|
@@ -471,13 +587,9 @@ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
|
|
|
471
587
|
return Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888)
|
|
472
588
|
}
|
|
473
589
|
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
return existing
|
|
478
|
-
existing?.destroy()
|
|
479
|
-
return Allocation.createFromBitmap(rs, src,
|
|
480
|
-
Allocation.MipmapControl.MIPMAP_NONE, Allocation.USAGE_SCRIPT)
|
|
590
|
+
private fun blurRadiusFromAmount(amount: Float): Float {
|
|
591
|
+
val t = amount.coerceIn(0f, 100f) / 100f
|
|
592
|
+
return (2f + t * 22f) // 2–24, StackBlur works well in this range per pass
|
|
481
593
|
}
|
|
482
594
|
|
|
483
595
|
private fun parseHexColor(s: String): Int? {
|
|
@@ -502,19 +614,11 @@ class BlurVibeViewApi31(context: Context) : ReactViewGroup(context) {
|
|
|
502
614
|
} catch (_: NumberFormatException) { null }
|
|
503
615
|
}
|
|
504
616
|
|
|
505
|
-
private fun blurRadiusFromAmount(amount: Float): Float {
|
|
506
|
-
// Linear 0→100 maps to 1→25 (RenderScript kernel max is 25).
|
|
507
|
-
// With BLUR_ROUNDS=4 passes the effective spread is radius × √4 = radius × 2,
|
|
508
|
-
// so blurAmount=100 gives effective spread of ~50px — properly frosted glass.
|
|
509
|
-
val t = amount.coerceIn(0f, 100f) / 100f
|
|
510
|
-
return (1f + t * 24f)
|
|
511
|
-
}
|
|
512
|
-
|
|
513
617
|
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {}
|
|
514
618
|
|
|
515
619
|
companion object {
|
|
516
|
-
private const val DOWNSAMPLE = 2f
|
|
517
|
-
private const val BLUR_ROUNDS =
|
|
620
|
+
private const val DOWNSAMPLE = 2f
|
|
621
|
+
private const val BLUR_ROUNDS = 3
|
|
518
622
|
const val PROGRESSIVE_NONE = 0
|
|
519
623
|
const val PROGRESSIVE_TOP_TO_BOTTOM = 1
|
|
520
624
|
const val PROGRESSIVE_BOTTOM_TO_TOP = 2
|
package/package.json
CHANGED