1 /*
<lambda>null2  * 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.ui.geometry.Offset
20 import androidx.compose.ui.geometry.Rect
21 import androidx.compose.ui.geometry.Size
22 import androidx.compose.ui.graphics.Matrix
23 import androidx.compose.ui.graphics.Outline
24 import androidx.compose.ui.graphics.Path
25 import androidx.compose.ui.graphics.Shape
26 import androidx.compose.ui.unit.Density
27 import androidx.compose.ui.unit.LayoutDirection
28 import androidx.graphics.shapes.Cubic
29 import androidx.graphics.shapes.Feature
30 import androidx.graphics.shapes.Morph
31 import androidx.graphics.shapes.RoundedPolygon
32 import androidx.graphics.shapes.TransformResult
33 
34 /**
35  * Utility functions providing more idiomatic ways of transforming RoundedPolygons and transforming
36  * shapes into a compose Path, for drawing them.
37  *
38  * This should in the future move into the compose library, maybe with additional API that makes it
39  * easier to create, draw, and animate from Compose apps.
40  *
41  * This code is just here for now prior to integration into compose
42  */
43 
44 /** Scales a shape (given as a List), creating a new List. */
45 fun List<Cubic>.scaled(scale: Float) = map {
46     it.transformed { x, y -> TransformResult(x * scale, y * scale) }
47 }
48 
49 /**
50  * Gets a [Path] representation for a [RoundedPolygon] shape, which can be used to draw the polygon.
51  *
52  * @param path an optional [Path] object which, if supplied, will avoid the function having to
53  *   create a new [Path] object
54  */
55 @JvmOverloads
RoundedPolygonnull56 fun RoundedPolygon.toPath(path: Path = Path()): Path {
57     pathFromCubics(path, cubics)
58     return path
59 }
60 
61 /**
62  * Gets a [Path] representation for a [Morph] shape. This [Path] can be used to draw the morph.
63  *
64  * @param progress a value from 0 to 1 that determines the morph's current shape, between the start
65  *   and end shapes provided at construction time. A value of 0 results in the start shape, a value
66  *   of 1 results in the end shape, and any value in between results in a shape which is a linear
67  *   interpolation between those two shapes. The range is generally [0..1] and values outside could
68  *   result in undefined shapes, but values close to (but outside) the range can be used to get an
69  *   exaggerated effect (e.g., for a bounce or overshoot animation).
70  * @param path an optional [Path] object which, if supplied, will avoid the function having to
71  *   create a new [Path] object
72  */
toPathnull73 fun Morph.toPath(progress: Float, path: Path = Path()): Path {
74     pathFromCubics(path, asCubics(progress))
75     return path
76 }
77 
78 /**
79  * Gets a [Path] representation for a [Feature] shape. This [Path] can be used to draw the feature.
80  *
81  * @param path an optional [Path] object which, if supplied, will avoid the function having to
82  *   create a new [Path] object
83  */
84 @JvmOverloads
toPathnull85 fun Feature.toPath(path: Path = Path()): Path {
86     pathFromCubics(path, cubics, false)
87     return path
88 }
89 
90 /**
91  * Returns the geometry of the given [cubics] in the given [path] object. This is used internally by
92  * the toPath functions, but we could consider exposing it as public API in case anyone was dealing
93  * directly with the cubics we create for our shapes.
94  */
pathFromCubicsnull95 private fun pathFromCubics(path: Path, cubics: List<Cubic>, closePath: Boolean = true) {
96     var first = true
97     path.rewind()
98     for (i in 0 until cubics.size) {
99         val cubic = cubics[i]
100         if (first) {
101             path.moveTo(cubic.anchor0X, cubic.anchor0Y)
102             first = false
103         }
104         path.cubicTo(
105             cubic.control0X,
106             cubic.control0Y,
107             cubic.control1X,
108             cubic.control1Y,
109             cubic.anchor1X,
110             cubic.anchor1Y
111         )
112     }
113     if (closePath) {
114         path.close()
115     }
116 }
117 
118 /** Transforms a [RoundedPolygon] with the given [Matrix] */
RoundedPolygonnull119 fun RoundedPolygon.transformed(matrix: Matrix): RoundedPolygon = transformed { x, y ->
120     val transformedPoint = matrix.map(Offset(x, y))
121     TransformResult(transformedPoint.x, transformedPoint.y)
122 }
123 
124 /** Calculates and returns the bounds of this [RoundedPolygon] as a [Rect] */
<lambda>null125 fun RoundedPolygon.getBounds() = calculateBounds().let { Rect(it[0], it[1], it[2], it[3]) }
126 
127 /** Calculates and returns the bounds of this [Morph] as a [Rect] */
<lambda>null128 fun Morph.getBounds() = calculateBounds().let { Rect(it[0], it[1], it[2], it[3]) }
129 
130 /**
131  * This class can be used to create a [Shape] object from a [RoundedPolygon]
132  *
133  * @param polygon The [RoundedPolygon] to be used for this [Shape]
134  * @param matrix An optional transformation matrix. If none is supplied, or null is passed as the
135  *   value, a transformation matrix will be calculated internally, based on the bounds of [polygon].
136  *   The result will be that [polygon] will be scaled and translated to fit within the size of the
137  *   [Shape].
138  */
139 class RoundedPolygonShape(
140     private val polygon: RoundedPolygon,
141     private var matrix: Matrix = Matrix()
142 ) : Shape {
143     private val path = Path()
144 
createOutlinenull145     override fun createOutline(
146         size: Size,
147         layoutDirection: LayoutDirection,
148         density: Density
149     ): Outline {
150         path.rewind()
151         polygon.toPath(path)
152         fitToViewport(path, polygon.getBounds(), size, matrix)
153         return Outline.Generic(path)
154     }
155 }
156 
157 /**
158  * This class can be used to create a [Shape] object from a [RoundedPolygon]
159  *
160  * @param morph The [Morph] to be used for this [Shape]
161  * @param progress a value from 0 to 1 that determines the morph's current shape, between the start
162  *   and end shapes provided at construction time. A value of 0 results in the start shape, a value
163  *   of 1 results in the end shape, and any value in between results in a shape which is a linear
164  *   interpolation between those two shapes. The range is generally [0..1] and values outside could
165  *   result in undefined shapes, but values close to (but outside) the range can be used to get an
166  *   exaggerated effect (e.g., for a bounce or overshoot animation).
167  * @param matrix An optional transformation matrix. If none is supplied, or null is passed as the
168  *   value, a transformation matrix will be calculated internally, based on the bounds of [morph].
169  *   The result will be that [morph] will be scaled and translated to fit within the size of the
170  *   [Shape].
171  */
172 class MorphShape(
173     private val morph: Morph,
174     private val progress: Float,
175     private var matrix: Matrix = Matrix()
176 ) : Shape {
177     private val path = Path()
178 
createOutlinenull179     override fun createOutline(
180         size: Size,
181         layoutDirection: LayoutDirection,
182         density: Density
183     ): Outline {
184         path.rewind()
185         morph.toPath(progress, path)
186         fitToViewport(path, morph.getBounds(), size, matrix)
187         return Outline.Generic(path)
188     }
189 }
190 
191 /**
192  * Scales and translates the given [path] to fit within the given [viewport], using the max
193  * dimension of [bounds] and min dimension of [viewport] to ensure that the path fits completely
194  * within the viewport.
195  *
196  * @param path the path to be transformed
197  * @param bounds the bounds of the shape represented by [path]
198  * @param viewport the area within which [path] will be transformed to fit
199  * @param matrix optional [Matrix] item which can be supplied to avoid creating a new matrix every
200  *   time the function is called.
201  */
fitToViewportnull202 fun fitToViewport(path: Path, bounds: Rect, viewport: Size, matrix: Matrix = Matrix()) {
203     matrix.reset()
204     val maxDimension = bounds.maxDimension
205     if (maxDimension > 0f) {
206         val scaleFactor = viewport.minDimension / maxDimension
207         val pathCenterX = bounds.left + bounds.width / 2
208         val pathCenterY = bounds.top + bounds.height / 2
209         matrix.translate(viewport.minDimension / 2, viewport.minDimension / 2)
210         matrix.scale(scaleFactor, scaleFactor)
211         matrix.translate(-pathCenterX, -pathCenterY)
212         path.transform(matrix)
213     }
214 }
215 
radialToCartesiannull216 fun radialToCartesian(radius: Float, angleRadians: Float, center: Offset = Offset.Zero) =
217     directionVector(angleRadians) * radius + center
218