docs: add implementation plan for heavy blur background

Made-with: Cursor
This commit is contained in:
cottongin
2026-03-18 13:28:17 -04:00
parent a5057be3c7
commit 7cd9d249d8

View 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 ~368378):
```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)`.