1 /*
2 * Copyright 2023 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17 package androidx.graphics.shapes.testcompose
18
19 import androidx.compose.foundation.clickable
20 import androidx.compose.foundation.layout.Box
21 import androidx.compose.foundation.layout.offset
22 import androidx.compose.foundation.layout.size
23 import androidx.compose.foundation.shape.CircleShape
24 import androidx.compose.material3.ColorScheme
25 import androidx.compose.runtime.Composable
26 import androidx.compose.ui.Modifier
27 import androidx.compose.ui.draw.clip
28 import androidx.compose.ui.draw.drawWithContent
29 import androidx.compose.ui.geometry.Offset
30 import androidx.compose.ui.graphics.Color
31 import androidx.compose.ui.graphics.drawscope.DrawScope
32 import androidx.compose.ui.graphics.drawscope.Fill
33 import androidx.compose.ui.graphics.drawscope.Stroke
34 import androidx.compose.ui.platform.LocalDensity
35 import androidx.compose.ui.unit.Dp
36 import androidx.compose.ui.unit.IntOffset
37 import androidx.compose.ui.unit.dp
38 import androidx.graphics.shapes.Cubic
39 import androidx.graphics.shapes.Feature
40 import kotlin.math.roundToInt
41
debugDrawCubicnull42 internal fun DrawScope.debugDrawCubic(bezier: Cubic, scheme: ColorScheme) {
43 // Draw red circles for start and end.
44 drawCircle(scheme.inverseSurface, radius = 6f, center = bezier.anchor0(), style = Stroke(2f))
45 drawCircle(scheme.inverseSurface, radius = 8f, center = bezier.anchor1(), style = Stroke(2f))
46
47 // Draw a circle for the first control point, and a line from start to it.
48 // The curve will start in this direction
49 drawLine(scheme.scrim, bezier.anchor0(), bezier.control0(), strokeWidth = 0f)
50 drawCircle(scheme.scrim, radius = 4f, center = bezier.control0(), style = Stroke(2f))
51
52 // Draw a circle for the second control point, and a line from it to the end.
53 // The curve will end in this direction
54 drawLine(scheme.scrim, bezier.control1(), bezier.anchor1(), strokeWidth = 0f)
55 drawCircle(scheme.scrim, radius = 4f, center = bezier.control1(), style = Stroke(2f))
56
57 // Draw dots along each curve
58 var t = .1f
59 while (t < 1f) {
60 drawCircle(scheme.primary, radius = 2f, center = bezier.pointOnCurve(t), style = Stroke(2f))
61 t += .1f
62 }
63 }
64
debugDrawFeaturenull65 internal fun DrawScope.debugDrawFeature(
66 feature: Feature,
67 colorScheme: FeatureColorScheme,
68 backgroundColor: Color,
69 radius: Float
70 ) {
71 val color = featureToColor(feature, colorScheme)
72 val representativePoint = featureRepresentativePoint(feature)
73
74 // Draw a clickable circle for the representative Point
75 drawCircle(color, radius = radius, center = representativePoint, style = Fill)
76
77 // With a bit of a background to suggest tapping is possible
78 drawCircle(
79 color.copy(0.2f),
80 radius = radius + (radius * 0.6f),
81 center = representativePoint,
82 style = Fill
83 )
84
85 // Finally add a border around the representative point
86 drawCircle(
87 backgroundColor,
88 radius = radius,
89 center = representativePoint,
90 style = Stroke(radius / 4)
91 )
92 }
93
94 @Composable
FeatureRepresentativePointnull95 internal fun FeatureRepresentativePoint(
96 modifier: Modifier = Modifier,
97 feature: Feature,
98 colorScheme: FeatureColorScheme,
99 backgroundColor: Color,
100 model: PanZoomRotateBoxState = PanZoomRotateBoxState(),
101 pointSize: Dp = 15.dp,
102 onClick: () -> Unit,
103 ) {
104 val radius = with(LocalDensity.current) { (pointSize / 2).roundToPx() }
105 val position = model.mapOut(featureRepresentativePoint(feature))
106
107 Box(
108 modifier
109 .offset {
110 IntOffset(
111 (position.x).roundToInt(),
112 (position.y).roundToInt(),
113 )
114 }
115 .drawWithContent {
116 drawContent()
117 debugDrawFeature(feature, colorScheme, backgroundColor, radius.toFloat())
118 }
119 )
120
121 Box(
122 modifier
123 .offset {
124 IntOffset(
125 (position.x - CLICKABLE_SCALE * radius).roundToInt(),
126 (position.y - CLICKABLE_SCALE * radius).roundToInt(),
127 )
128 }
129 .size(pointSize * CLICKABLE_SCALE)
130 .clip(CircleShape)
131 .clickable(onClick = onClick)
132 )
133 }
134
featureToColornull135 internal fun featureToColor(feature: Feature, scheme: FeatureColorScheme): Color =
136 if (feature.isEdge) {
137 scheme.edgeColor
138 } else if (feature.isConvexCorner) {
139 scheme.convexColor
140 } else {
141 scheme.concaveColor
142 }
143
144 // TODO: b/378441547 - Remove if explicit / exposed by default
featureRepresentativePointnull145 internal fun featureRepresentativePoint(feature: Feature): Offset =
146 (feature.cubics.first().anchor0() + feature.cubics.last().anchor1()) / 2f
147
148 internal fun Cubic.anchor0() = Offset(anchor0X, anchor0Y)
149
150 internal fun Cubic.control0() = Offset(control0X, control0Y)
151
152 internal fun Cubic.control1() = Offset(control1X, control1Y)
153
154 internal fun Cubic.anchor1() = Offset(anchor1X, anchor1Y)
155
156 internal const val CLICKABLE_SCALE = 1.8f
157