# 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)`.