docs: add implementation plan for heavy blur background
Made-with: Cursor
This commit is contained in:
291
docs/plans/2026-03-18-heavy-blur-background.md
Normal file
291
docs/plans/2026-03-18-heavy-blur-background.md
Normal file
@@ -0,0 +1,291 @@
|
||||
# Heavy Blur Background Implementation Plan
|
||||
|
||||
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
||||
|
||||
**Goal:** Replace the Cloudy blur library with an inlined Stackblur `BlurTransformation` applied via Coil's pipeline, producing a heavily blurred background where album art text is completely illegible.
|
||||
|
||||
**Architecture:** A self-contained `BlurTransformation` class is added to the project's util package. It is applied as a Coil `ImageRequest` transformation in `BlurredBackground`, replacing the `.cloudy()` modifier. Cloudy is then removed from all dependency files.
|
||||
|
||||
**Tech Stack:** Kotlin, Jetpack Compose, Coil 3 (`io.coil-kt.coil3`), Android Bitmap APIs
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Add BlurTransformation utility class
|
||||
|
||||
**Files:**
|
||||
- Create: `app/src/main/java/xyz/cottongin/radio247/ui/util/BlurTransformation.kt`
|
||||
|
||||
**Step 1: Create the file with the Stackblur implementation**
|
||||
|
||||
```kotlin
|
||||
package xyz.cottongin.radio247.ui.util
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import coil3.size.Size
|
||||
import coil3.transform.Transformation
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class BlurTransformation(
|
||||
private val radius: Int = 25,
|
||||
private val scale: Float = 0.1f
|
||||
) : Transformation() {
|
||||
|
||||
override val cacheKey: String = "${javaClass.name}-$radius-$scale"
|
||||
|
||||
override suspend fun transform(input: Bitmap, size: Size): Bitmap =
|
||||
input.stackBlur(scale, radius) ?: input
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
return other is BlurTransformation && radius == other.radius && scale == other.scale
|
||||
}
|
||||
|
||||
override fun hashCode(): Int = 31 * radius.hashCode() + scale.hashCode()
|
||||
}
|
||||
|
||||
private suspend fun Bitmap.stackBlur(scale: Float, radius: Int): Bitmap? =
|
||||
withContext(Dispatchers.IO) {
|
||||
var sentBitmap = this@stackBlur
|
||||
val width = (sentBitmap.width * scale).roundToInt()
|
||||
val height = (sentBitmap.height * scale).roundToInt()
|
||||
sentBitmap = Bitmap.createScaledBitmap(sentBitmap, width, height, false)
|
||||
val bitmap = sentBitmap.copy(sentBitmap.config ?: Bitmap.Config.ARGB_8888, true)
|
||||
if (radius < 1) return@withContext null
|
||||
val w = bitmap.width
|
||||
val h = bitmap.height
|
||||
val pix = IntArray(w * h)
|
||||
bitmap.getPixels(pix, 0, w, 0, 0, w, h)
|
||||
val wm = w - 1
|
||||
val hm = h - 1
|
||||
val wh = w * h
|
||||
val div = radius + radius + 1
|
||||
val r = IntArray(wh)
|
||||
val g = IntArray(wh)
|
||||
val b = IntArray(wh)
|
||||
var rsum: Int; var gsum: Int; var bsum: Int
|
||||
var x: Int; var y: Int; var i: Int; var p: Int; var yp: Int; var yi: Int
|
||||
val vmin = IntArray(w.coerceAtLeast(h))
|
||||
var divsum = div + 1 shr 1
|
||||
divsum *= divsum
|
||||
val dv = IntArray(256 * divsum) { it / divsum }
|
||||
yi = 0; var yw = 0
|
||||
val stack = Array(div) { IntArray(3) }
|
||||
var stackpointer: Int; var stackstart: Int; var sir: IntArray; var rbs: Int
|
||||
val r1 = radius + 1
|
||||
var routsum: Int; var goutsum: Int; var boutsum: Int
|
||||
var rinsum: Int; var ginsum: Int; var binsum: Int
|
||||
y = 0
|
||||
while (y < h) {
|
||||
bsum = 0; gsum = 0; rsum = 0; boutsum = 0; goutsum = 0; routsum = 0
|
||||
binsum = 0; ginsum = 0; rinsum = 0
|
||||
i = -radius
|
||||
while (i <= radius) {
|
||||
p = pix[yi + wm.coerceAtMost(i.coerceAtLeast(0))]
|
||||
sir = stack[i + radius]
|
||||
sir[0] = p and 0xff0000 shr 16; sir[1] = p and 0x00ff00 shr 8; sir[2] = p and 0x0000ff
|
||||
rbs = r1 - abs(i)
|
||||
rsum += sir[0] * rbs; gsum += sir[1] * rbs; bsum += sir[2] * rbs
|
||||
if (i > 0) { rinsum += sir[0]; ginsum += sir[1]; binsum += sir[2] }
|
||||
else { routsum += sir[0]; goutsum += sir[1]; boutsum += sir[2] }
|
||||
i++
|
||||
}
|
||||
stackpointer = radius; x = 0
|
||||
while (x < w) {
|
||||
r[yi] = dv[rsum]; g[yi] = dv[gsum]; b[yi] = dv[bsum]
|
||||
rsum -= routsum; gsum -= goutsum; bsum -= boutsum
|
||||
stackstart = stackpointer - radius + div
|
||||
sir = stack[stackstart % div]
|
||||
routsum -= sir[0]; goutsum -= sir[1]; boutsum -= sir[2]
|
||||
if (y == 0) vmin[x] = (x + radius + 1).coerceAtMost(wm)
|
||||
p = pix[yw + vmin[x]]
|
||||
sir[0] = p and 0xff0000 shr 16; sir[1] = p and 0x00ff00 shr 8; sir[2] = p and 0x0000ff
|
||||
rinsum += sir[0]; ginsum += sir[1]; binsum += sir[2]
|
||||
rsum += rinsum; gsum += ginsum; bsum += binsum
|
||||
stackpointer = (stackpointer + 1) % div
|
||||
sir = stack[stackpointer % div]
|
||||
routsum += sir[0]; goutsum += sir[1]; boutsum += sir[2]
|
||||
rinsum -= sir[0]; ginsum -= sir[1]; binsum -= sir[2]
|
||||
yi++; x++
|
||||
}
|
||||
yw += w; y++
|
||||
}
|
||||
x = 0
|
||||
while (x < w) {
|
||||
bsum = 0; gsum = 0; rsum = 0; boutsum = 0; goutsum = 0; routsum = 0
|
||||
binsum = 0; ginsum = 0; rinsum = 0
|
||||
yp = -radius * w; i = -radius
|
||||
while (i <= radius) {
|
||||
yi = 0.coerceAtLeast(yp) + x
|
||||
sir = stack[i + radius]
|
||||
sir[0] = r[yi]; sir[1] = g[yi]; sir[2] = b[yi]
|
||||
rbs = r1 - abs(i)
|
||||
rsum += r[yi] * rbs; gsum += g[yi] * rbs; bsum += b[yi] * rbs
|
||||
if (i > 0) { rinsum += sir[0]; ginsum += sir[1]; binsum += sir[2] }
|
||||
else { routsum += sir[0]; goutsum += sir[1]; boutsum += sir[2] }
|
||||
if (i < hm) yp += w
|
||||
i++
|
||||
}
|
||||
yi = x; stackpointer = radius; y = 0
|
||||
while (y < h) {
|
||||
pix[yi] = -0x1000000 and pix[yi] or (dv[rsum] shl 16) or (dv[gsum] shl 8) or dv[bsum]
|
||||
rsum -= routsum; gsum -= goutsum; bsum -= boutsum
|
||||
stackstart = stackpointer - radius + div
|
||||
sir = stack[stackstart % div]
|
||||
routsum -= sir[0]; goutsum -= sir[1]; boutsum -= sir[2]
|
||||
if (x == 0) vmin[y] = (y + r1).coerceAtMost(hm) * w
|
||||
p = x + vmin[y]
|
||||
sir[0] = r[p]; sir[1] = g[p]; sir[2] = b[p]
|
||||
rinsum += sir[0]; ginsum += sir[1]; binsum += sir[2]
|
||||
rsum += rinsum; gsum += ginsum; bsum += binsum
|
||||
stackpointer = (stackpointer + 1) % div
|
||||
sir = stack[stackpointer]
|
||||
routsum += sir[0]; goutsum += sir[1]; boutsum += sir[2]
|
||||
rinsum -= sir[0]; ginsum -= sir[1]; binsum -= sir[2]
|
||||
yi += w; y++
|
||||
}
|
||||
x++
|
||||
}
|
||||
bitmap.setPixels(pix, 0, w, 0, 0, w, h)
|
||||
sentBitmap.recycle()
|
||||
bitmap
|
||||
}
|
||||
```
|
||||
|
||||
> **Note on Coil 3 API:** Coil 3 uses `coil3.transform.Transformation` (abstract class, not interface) and `coil3.size.Size`. The `transform()` method signature is `suspend fun transform(input: Bitmap, size: Size): Bitmap`. Verify these imports compile — if `Transformation` is an interface in the version in use, remove the `()` after `Transformation`.
|
||||
|
||||
**Step 2: Verify the file compiles**
|
||||
|
||||
```bash
|
||||
cd /path/to/Android-247-Radio
|
||||
./gradlew :app:compileDebugKotlin 2>&1 | tail -20
|
||||
```
|
||||
|
||||
Expected: BUILD SUCCESSFUL (or only pre-existing warnings, no new errors)
|
||||
|
||||
**Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add app/src/main/java/xyz/cottongin/radio247/ui/util/BlurTransformation.kt
|
||||
git commit -m "feat: add Stackblur BlurTransformation utility"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: Update BlurredBackground to use BlurTransformation
|
||||
|
||||
**Files:**
|
||||
- Modify: `app/src/main/java/xyz/cottongin/radio247/ui/screens/nowplaying/NowPlayingScreen.kt:359-388`
|
||||
|
||||
**Step 1: Update the `BlurredBackground` composable**
|
||||
|
||||
Find this block (lines ~368–378):
|
||||
|
||||
```kotlin
|
||||
AsyncImage(
|
||||
model = ImageRequest.Builder(context)
|
||||
.data(artworkUrl)
|
||||
.size(Size(10, 10))
|
||||
.build(),
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.Crop,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.cloudy(radius = 25)
|
||||
)
|
||||
```
|
||||
|
||||
Replace with:
|
||||
|
||||
```kotlin
|
||||
AsyncImage(
|
||||
model = ImageRequest.Builder(context)
|
||||
.data(artworkUrl)
|
||||
.transformations(BlurTransformation(radius = 25, scale = 0.1f))
|
||||
.build(),
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.Crop,
|
||||
modifier = Modifier.fillMaxSize()
|
||||
)
|
||||
```
|
||||
|
||||
**Step 2: Update imports in `NowPlayingScreen.kt`**
|
||||
|
||||
Remove:
|
||||
```kotlin
|
||||
import com.skydoves.cloudy.cloudy
|
||||
```
|
||||
|
||||
Add (if not auto-resolved by IDE):
|
||||
```kotlin
|
||||
import xyz.cottongin.radio247.ui.util.BlurTransformation
|
||||
```
|
||||
|
||||
Also remove the `coil.size.Size` import if it was only used for the `size(10, 10)` call and is no longer referenced. Check for any remaining `Size` usage in the file first.
|
||||
|
||||
**Step 3: Verify the file compiles**
|
||||
|
||||
```bash
|
||||
./gradlew :app:compileDebugKotlin 2>&1 | tail -20
|
||||
```
|
||||
|
||||
Expected: BUILD SUCCESSFUL
|
||||
|
||||
**Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add app/src/main/java/xyz/cottongin/radio247/ui/screens/nowplaying/NowPlayingScreen.kt
|
||||
git commit -m "feat: replace cloudy blur with BlurTransformation on player background"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: Remove Cloudy dependency
|
||||
|
||||
**Files:**
|
||||
- Modify: `app/build.gradle.kts`
|
||||
- Modify: `gradle/libs.versions.toml`
|
||||
|
||||
**Step 1: Remove from `app/build.gradle.kts`**
|
||||
|
||||
Remove this line:
|
||||
```kotlin
|
||||
implementation(libs.cloudy)
|
||||
```
|
||||
|
||||
**Step 2: Remove from `gradle/libs.versions.toml`**
|
||||
|
||||
Remove the version entry:
|
||||
```toml
|
||||
cloudy = "0.2.7"
|
||||
```
|
||||
|
||||
Remove the library entry:
|
||||
```toml
|
||||
cloudy = { group = "com.github.skydoves", name = "cloudy", version.ref = "cloudy" }
|
||||
```
|
||||
|
||||
**Step 3: Sync and verify full build**
|
||||
|
||||
```bash
|
||||
./gradlew :app:assembleDebug 2>&1 | tail -30
|
||||
```
|
||||
|
||||
Expected: BUILD SUCCESSFUL
|
||||
|
||||
**Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add app/build.gradle.kts gradle/libs.versions.toml
|
||||
git commit -m "chore: remove Cloudy blur library dependency"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Verification
|
||||
|
||||
After all tasks complete, install on device/emulator and navigate to the Now Playing screen. Album art text should be completely illegible and no sharp pixel edges should be visible in the background.
|
||||
|
||||
If the blur is still not strong enough, increase `scale` down (e.g. `0.05f`) or increase `radius` (e.g. `radius = 50`) in `BlurTransformation(radius = 25, scale = 0.1f)`.
|
||||
Reference in New Issue
Block a user