@@ -3,75 +3,170 @@ package com.stephenwanjala.multiply.game.components
33import androidx.compose.foundation.shape.RoundedCornerShape
44import androidx.compose.runtime.Composable
55import androidx.compose.ui.Modifier
6- import androidx.compose.ui.composed
76import androidx.compose.ui.draw.drawBehind
8- import androidx.compose.ui.draw.drawWithCache
97import androidx.compose.ui.geometry.Offset
8+ import androidx.compose.ui.geometry.Size
109import androidx.compose.ui.graphics.BlendMode
1110import androidx.compose.ui.graphics.Brush
1211import androidx.compose.ui.graphics.Color
12+ import androidx.compose.ui.graphics.Outline
1313import androidx.compose.ui.graphics.Path
1414import androidx.compose.ui.graphics.Shape
1515import androidx.compose.ui.graphics.drawOutline
16+ import androidx.compose.ui.graphics.drawscope.ContentDrawScope
1617import androidx.compose.ui.graphics.drawscope.Stroke
1718import androidx.compose.ui.graphics.drawscope.translate
19+ import androidx.compose.ui.node.DrawModifierNode
20+ import androidx.compose.ui.node.ModifierNodeElement
21+ import androidx.compose.ui.platform.InspectorInfo
1822import androidx.compose.ui.unit.Dp
23+ import androidx.compose.ui.unit.LayoutDirection
1924import androidx.compose.ui.unit.dp
2025
26+ /* *
27+ **
28+ * Applies a neumorphic shadow effect to the component.
29+ *
30+ * @param offset The offset distance for the shadow
31+ * @param blurRadius The blur radius (note: actual blur requires API 31+, this uses alpha for effect)
32+ * @param shape The shape of the shadow
33+ * @param lightColor The color of the light shadow
34+ * @param darkColor The color of the dark shadow
35+ * @param inverted Whether to invert the shadow direction (for pressed effect)
36+ */
2137fun Modifier.neumorphicShadow (
2238 offset : Dp = 6.dp,
2339 blurRadius : Dp = 6.dp,
2440 shape : Shape = RoundedCornerShape (8.dp),
2541 lightColor : Color = Color .White .copy(alpha = 0.7f),
2642 darkColor : Color = Color .Black .copy(alpha = 0.2f),
2743 inverted : Boolean = false
28- ): Modifier =
29- composed {
30- // We use drawWithCache for better performance as it caches the outline
31- // when the size or shape remains the same.
32- Modifier .drawWithCache {
33- val shadowOffsetPx = offset.toPx()
34- blurRadius.toPx()
35-
36- // For a "soft" shadow, we don't draw with Stroke directly on the outline.
37- // Instead, we translate the drawing area and draw the outline itself.
38- // The blurring effect is typically handled by RenderEffect (requires API 31+)
39- // or by drawing multiple slightly offset, transparent layers.
40- // For a simpler, cross-API neumorphic look, we'll draw the solid shape
41- // with an offset and rely on the alpha for the "blur" feel without actual blur.
42-
43- val outline = shape.createOutline(size, layoutDirection, this )
44-
45- onDrawBehind {
46- val lightOffset = if (inverted) Offset (
47- shadowOffsetPx,
48- shadowOffsetPx
49- ) else Offset (- shadowOffsetPx, - shadowOffsetPx)
50- val darkOffset = if (inverted) Offset (- shadowOffsetPx, - shadowOffsetPx) else Offset (
51- shadowOffsetPx,
52- shadowOffsetPx
53- )
54-
55- // Draw dark shadow
56- translate(left = darkOffset.x, top = darkOffset.y) {
57- drawOutline(
58- outline = outline,
59- color = darkColor,
60- alpha = darkColor.alpha
61- )
62- }
44+ ): Modifier = this then NeumorphicShadowElement (
45+ offset = offset,
46+ blurRadius = blurRadius,
47+ shape = shape,
48+ lightColor = lightColor,
49+ darkColor = darkColor,
50+ inverted = inverted
51+ )
6352
64- // Draw light shadow
65- translate(left = lightOffset.x, top = lightOffset.y) {
66- drawOutline(
67- outline = outline,
68- color = lightColor,
69- alpha = lightColor.alpha
70- )
71- }
72- }
53+ /* *
54+ * ModifierNodeElement that creates and updates NeumorphicShadowNode
55+ */
56+ private data class NeumorphicShadowElement (
57+ val offset : Dp ,
58+ val blurRadius : Dp ,
59+ val shape : Shape ,
60+ val lightColor : Color ,
61+ val darkColor : Color ,
62+ val inverted : Boolean
63+ ) : ModifierNodeElement<NeumorphicShadowNode>() {
64+
65+ override fun create (): NeumorphicShadowNode {
66+ return NeumorphicShadowNode (
67+ offset = offset,
68+ blurRadius = blurRadius,
69+ shape = shape,
70+ lightColor = lightColor,
71+ darkColor = darkColor,
72+ inverted = inverted
73+ )
74+ }
75+
76+ override fun update (node : NeumorphicShadowNode ) {
77+ node.offset = offset
78+ node.blurRadius = blurRadius
79+ node.shape = shape
80+ node.lightColor = lightColor
81+ node.darkColor = darkColor
82+ node.inverted = inverted
83+ }
84+
85+ override fun InspectorInfo.inspectableProperties () {
86+ name = " neumorphicShadow"
87+ properties[" offset" ] = offset
88+ properties[" blurRadius" ] = blurRadius
89+ properties[" shape" ] = shape
90+ properties[" lightColor" ] = lightColor
91+ properties[" darkColor" ] = darkColor
92+ properties[" inverted" ] = inverted
93+ }
94+ }
95+
96+ /* *
97+ * DrawModifierNode that performs the neumorphic shadow drawing
98+ */
99+ private class NeumorphicShadowNode (
100+ var offset : Dp ,
101+ var blurRadius : Dp ,
102+ var shape : Shape ,
103+ var lightColor : Color ,
104+ var darkColor : Color ,
105+ var inverted : Boolean
106+ ) : Modifier.Node(), DrawModifierNode {
107+
108+ private var cachedOutline: Outline ? = null
109+ private var cachedSize: Size ? = null
110+ private var cachedLayoutDirection: LayoutDirection ? = null
111+
112+ override fun ContentDrawScope.draw () {
113+ val shadowOffsetPx = offset.toPx()
114+ val currentSize = size
115+
116+ // Cache the outline for better performance
117+ if (cachedOutline == null ||
118+ cachedSize != currentSize ||
119+ cachedLayoutDirection != layoutDirection
120+ ) {
121+ cachedOutline = shape.createOutline(currentSize, layoutDirection, this )
122+ cachedSize = currentSize
123+ cachedLayoutDirection = layoutDirection
73124 }
125+
126+ val outline = cachedOutline!!
127+
128+ // Calculate shadow offsets based on inverted state
129+ val lightOffset = if (inverted) {
130+ Offset (shadowOffsetPx, shadowOffsetPx)
131+ } else {
132+ Offset (- shadowOffsetPx, - shadowOffsetPx)
133+ }
134+
135+ val darkOffset = if (inverted) {
136+ Offset (- shadowOffsetPx, - shadowOffsetPx)
137+ } else {
138+ Offset (shadowOffsetPx, shadowOffsetPx)
139+ }
140+
141+ // Draw dark shadow
142+ translate(left = darkOffset.x, top = darkOffset.y) {
143+ drawOutline(
144+ outline = outline,
145+ color = darkColor,
146+ alpha = darkColor.alpha
147+ )
148+ }
149+
150+ // Draw light shadow
151+ translate(left = lightOffset.x, top = lightOffset.y) {
152+ drawOutline(
153+ outline = outline,
154+ color = lightColor,
155+ alpha = lightColor.alpha
156+ )
157+ }
158+
159+ // Draw the actual content on top of shadows
160+ drawContent()
161+ }
162+
163+ override fun onDetach () {
164+ // Clear cache when node is detached
165+ cachedOutline = null
166+ cachedSize = null
167+ cachedLayoutDirection = null
74168 }
169+ }
75170
76171
77172// Background Effects
0 commit comments