11package com.flipcash.app.bills
22
3+ import android.R.attr.fillType
34import android.annotation.SuppressLint
45import android.graphics.Bitmap
56import android.graphics.BitmapFactory
@@ -32,29 +33,46 @@ import androidx.compose.runtime.remember
3233import androidx.compose.ui.Alignment
3334import androidx.compose.ui.Modifier
3435import androidx.compose.ui.draw.alpha
36+ import androidx.compose.ui.draw.clip
3537import androidx.compose.ui.draw.clipToBounds
38+ import androidx.compose.ui.draw.drawBehind
39+ import androidx.compose.ui.draw.drawWithContent
3640import androidx.compose.ui.draw.rotate
3741import androidx.compose.ui.geometry.Offset
42+ import androidx.compose.ui.geometry.Rect
43+ import androidx.compose.ui.geometry.Size
3844import androidx.compose.ui.graphics.BlendMode
3945import androidx.compose.ui.graphics.Brush
4046import androidx.compose.ui.graphics.Color
47+ import androidx.compose.ui.graphics.Color.Companion.Black
4148import androidx.compose.ui.graphics.ColorFilter
49+ import androidx.compose.ui.graphics.CompositingStrategy
4250import androidx.compose.ui.graphics.ImageBitmap
51+ import androidx.compose.ui.graphics.Outline
52+ import androidx.compose.ui.graphics.Path
53+ import androidx.compose.ui.graphics.PathFillType
54+ import androidx.compose.ui.graphics.Shape
55+ import androidx.compose.ui.graphics.SolidColor
4356import androidx.compose.ui.graphics.asImageBitmap
4457import androidx.compose.ui.graphics.compositeOver
4558import androidx.compose.ui.graphics.drawscope.DrawScope
59+ import androidx.compose.ui.graphics.drawscope.clipPath
60+ import androidx.compose.ui.graphics.graphicsLayer
61+ import androidx.compose.ui.graphics.lerp
4662import androidx.compose.ui.layout.ContentScale
4763import androidx.compose.ui.platform.LocalContext
4864import androidx.compose.ui.platform.LocalResources
4965import androidx.compose.ui.res.imageResource
5066import androidx.compose.ui.res.painterResource
5167import androidx.compose.ui.text.TextStyle
5268import androidx.compose.ui.text.font.FontWeight
69+ import androidx.compose.ui.unit.Density
5370import androidx.compose.ui.unit.Dp
5471import androidx.compose.ui.unit.DpOffset
5572import androidx.compose.ui.unit.DpSize
5673import androidx.compose.ui.unit.IntOffset
5774import androidx.compose.ui.unit.IntSize
75+ import androidx.compose.ui.unit.LayoutDirection
5876import androidx.compose.ui.unit.TextUnit
5977import androidx.compose.ui.unit.dp
6078import androidx.compose.ui.unit.isSpecified
@@ -76,6 +94,7 @@ import com.getcode.ui.core.punchRectangle
7694import com.getcode.ui.utils.Geometry
7795import com.getcode.ui.utils.deriveTargetColor
7896import com.getcode.ui.utils.hexToColor
97+ import com.getcode.ui.utils.hls
7998import com.getcode.ui.utils.nonScaledSp
8099import kotlin.math.ceil
81100import kotlin.math.roundToInt
@@ -127,7 +146,7 @@ private object CashBillDefaults {
127146 fun punchBrushIn (punch : Punch , token : Token ): Brush {
128147 val billCustomizations = token.billCustomizations
129148 if (billCustomizations?.background == null ) {
130- val color = Color . Black .copy(0.15f )
149+ val color = Black .copy(0.15f )
131150 .compositeOver(CodeTheme .colors.cashBillColor.copy(alpha = CodeBackgroundOpacity ))
132151 return Brush .verticalGradient(
133152 colors = listOf (color, color)
@@ -138,7 +157,7 @@ private object CashBillDefaults {
138157 is BillBackground .Gradient -> {
139158 when (punch) {
140159 Punch .SecurityStrip -> {
141- val color = Color . Black .copy(0.15f )
160+ val color = Black .copy(0.15f )
142161 .compositeOver(
143162 hexToColor(bg.colors.first())
144163 .copy(alpha = CodeBackgroundOpacity )
@@ -150,30 +169,15 @@ private object CashBillDefaults {
150169 }
151170
152171 Punch .Code -> {
153- val bgColors = if (bg.colors.size == 3 ) {
154- bg.colors.slice(listOf (0 , 2 ))
155- } else {
156- bg.colors
157- }
158-
159- Brush .verticalGradient(
160- colors = bgColors.map { hexToColor(it) }
161- .map {
162- deriveTargetColor(
163- sourceColor = it,
164- targetLightness = - 0.314f ,
165- targetSaturation = - 0.203f
166- ).copy(alpha = 0.62f )
167- }
168- )
172+ punchBrushFrom(bg.colors.map { hexToColor(it) })
169173 }
170174 }
171175 }
172176
173177 is BillBackground .Solid -> {
174178 when (punch) {
175179 Punch .SecurityStrip -> {
176- val color = Color . Black .copy(0.15f )
180+ val color = Black .copy(0.15f )
177181 .compositeOver(hexToColor(bg.colorHex).copy(alpha = 0.62f ))
178182
179183 Brush .verticalGradient(
@@ -182,15 +186,7 @@ private object CashBillDefaults {
182186 }
183187
184188 Punch .Code -> {
185- val color = deriveTargetColor(
186- sourceColor = hexToColor(bg.colorHex),
187- targetLightness = - 0.314f ,
188- targetSaturation = - 0.203f
189- ).copy(alpha = 0.62f )
190-
191- Brush .verticalGradient(
192- colors = listOf (color, color)
193- )
189+ punchBrushFrom( listOf (hexToColor(bg.colorHex)))
194190 }
195191 }
196192 }
@@ -313,86 +309,103 @@ internal fun CashBill(
313309 .aspectRatio(CashBillDefaults .AspectRatio , matchHeightConstraintsFirst = true )
314310 .fillMaxHeight()
315311 .fillMaxWidth(0.95f )
316- .background(CashBillDefaults .billColor(token))
317312 .clipToBounds()
318313 ) {
319314 val geometry = remember(maxWidth, maxHeight) {
320315 CashBillGeometry (maxWidth, maxHeight)
321316 }
322317
323- if (hasCustomTexture) {
324- val context = LocalContext .current
325- val resources = LocalResources .current
326- val pattern = runCatching {
327- resources.getIdentifier(" bill_texture_${customTexture.index} " , " drawable" , context.packageName)
328- }.getOrNull()
318+ val punchShape = remember(geometry) {
319+ BillPunchShape (codeSize = geometry.codeSize)
320+ }
329321
330- if (pattern != null ) {
331- Box (
332- modifier = Modifier
333- .fillMaxSize()
334- .patternBlend(
335- pattern = ImageBitmap .imageResource(resources, pattern),
336- blendMode = when (customTexture.blendMode) {
337- PlaygroundBlendMode .Normal -> null
338- PlaygroundBlendMode .Lighten -> BlendMode .Lighten
339- PlaygroundBlendMode .Screen -> BlendMode .Screen
340- PlaygroundBlendMode .ColorDodge -> BlendMode .ColorDodge
341- PlaygroundBlendMode .PlusLighter -> BlendMode .Softlight
342- },
343- strength = customTexture.strength,
344- ),
345- )
346- }
347- } else {
348- // Hexagons
349- BillDecorImage (
350- modifier = Modifier
351- .fillMaxSize(),
352- image = loadBillAsset(R .drawable.ic_bill_hexagons),
353- blendMode = BlendMode .Multiply ,
354- alpha = 0.6f ,
355- )
322+ // Background with punch hole
323+ Box (
324+ modifier = Modifier
325+ .fillMaxSize()
326+ .background(CashBillDefaults .billColor(token), punchShape)
327+ )
356328
357- // Grid pattern
358- BillDecorImage (
359- modifier = Modifier
360- .fillMaxSize(),
361- image = loadBillAsset(R .drawable.ic_bill_grid),
362- size = DpSize (width = geometry.gridWidth, height = geometry.gridHeight),
363- topLeft = Offset (
364- x = geometry.gridPosition.x,
365- y = geometry.gridPosition.y,
329+ // Texture layers — clipped with even-odd punch
330+ Box (
331+ modifier = Modifier
332+ .fillMaxSize()
333+ .clip(punchShape)
334+ ) {
335+ if (hasCustomTexture) {
336+ val context = LocalContext .current
337+ val resources = LocalResources .current
338+ val pattern = runCatching {
339+ resources.getIdentifier(" bill_texture_${customTexture.index} " , " drawable" , context.packageName)
340+ }.getOrNull()
341+
342+ if (pattern != null ) {
343+ Box (
344+ modifier = Modifier
345+ .fillMaxSize()
346+ .patternBlend(
347+ pattern = ImageBitmap .imageResource(resources, pattern),
348+ blendMode = when (customTexture.blendMode) {
349+ PlaygroundBlendMode .Normal -> null
350+ PlaygroundBlendMode .Lighten -> BlendMode .Lighten
351+ PlaygroundBlendMode .Screen -> BlendMode .Screen
352+ PlaygroundBlendMode .ColorDodge -> BlendMode .ColorDodge
353+ PlaygroundBlendMode .PlusLighter -> BlendMode .Softlight
354+ },
355+ strength = customTexture.strength,
356+ ),
357+ )
358+ }
359+ } else {
360+ // Hexagons
361+ BillDecorImage (
362+ modifier = Modifier
363+ .fillMaxSize(),
364+ image = loadBillAsset(R .drawable.ic_bill_hexagons),
365+ blendMode = BlendMode .Multiply ,
366+ alpha = 0.6f ,
366367 )
367- )
368- }
369368
370- // Globe
371- Image (
372- modifier = Modifier
373- .fillMaxHeight()
374- .requiredWidth(geometry.globeWidth)
375- .offset {
376- IntOffset (
377- x = geometry.globePosition.x.toInt() ,
378- y = geometry.globePosition.y.toInt()
369+ // Grid pattern
370+ BillDecorImage (
371+ modifier = Modifier
372+ .fillMaxSize(),
373+ image = loadBillAsset( R .drawable.ic_bill_grid),
374+ size = DpSize (width = geometry.gridWidth, height = geometry.gridHeight),
375+ topLeft = Offset (
376+ x = geometry.gridPosition.x ,
377+ y = geometry.gridPosition.y,
379378 )
380- },
381- painter = painterResource(R .drawable.ic_bill_globe),
382- contentDescription = null
383- )
379+ )
380+ }
384381
385- if (! hasCustomTexture) {
386- // Waves
382+ // Globe
387383 Image (
388384 modifier = Modifier
389- .requiredWidth(geometry.globeWidth)
390385 .fillMaxHeight()
391- .offset { IntOffset (x = geometry.wavesPosition.x.toInt(), y = 0 ) },
392- contentDescription = null ,
393- contentScale = ContentScale .FillBounds ,
394- painter = painterResource(R .drawable.ic_bill_waves),
386+ .requiredWidth(geometry.globeWidth)
387+ .offset {
388+ IntOffset (
389+ x = geometry.globePosition.x.toInt(),
390+ y = geometry.globePosition.y.toInt()
391+ )
392+ },
393+ painter = painterResource(R .drawable.ic_bill_globe),
394+ contentDescription = null
395395 )
396+
397+ if (! hasCustomTexture) {
398+ // Waves
399+ Image (
400+ modifier = Modifier
401+ .requiredWidth(geometry.globeWidth)
402+ .fillMaxHeight()
403+ .offset { IntOffset (x = geometry.wavesPosition.x.toInt(), y = 0 ) },
404+ contentDescription = null ,
405+ contentScale = ContentScale .FillBounds ,
406+ painter = painterResource(R .drawable.ic_bill_waves),
407+ )
408+ }
396409 }
397410
398411 // Security strip
@@ -580,7 +593,6 @@ private fun BillCode(
580593 modifier = modifier
581594 .punchCircle(
582595 brush = CashBillDefaults .punchBrushIn(punch = Punch .Code , token),
583- // blendMode = BlendMode.SrcOver,
584596 ),
585597 contentAlignment = Alignment .Center
586598 ) {
@@ -605,3 +617,38 @@ private fun loadBillAsset(drawableRes: Int): ImageBitmap {
605617 option
606618 ).asImageBitmap()
607619}
620+
621+ private class BillPunchShape (
622+ private val codeSize : Dp ,
623+ ) : Shape {
624+ override fun createOutline (
625+ size : Size , layoutDirection : LayoutDirection , density : Density
626+ ): Outline {
627+ val radius = with (density) { (codeSize / 2f ).toPx() }
628+ val path = Path ().apply {
629+ fillType = PathFillType .EvenOdd
630+ addRect(Rect (Offset .Zero , size))
631+ addOval(
632+ Rect (
633+ center = Offset (size.width / 2f , size.height / 2f ),
634+ radius = radius
635+ )
636+ )
637+ }
638+ return Outline .Generic (path)
639+ }
640+ }
641+
642+ fun punchColorsFrom (billColors : List <Color >): List <Color > {
643+ return billColors.map { color ->
644+ lerp(color, Color .Black , 0.30f )
645+ }
646+ }
647+
648+ fun punchBrushFrom (billColors : List <Color >): Brush {
649+ val punched = punchColorsFrom(billColors)
650+ return when {
651+ punched.size == 1 -> SolidColor (punched[0 ])
652+ else -> Brush .verticalGradient(punched.map { it.copy(0.83f ) })
653+ }
654+ }
0 commit comments