1 /*
<lambda>null2  * Copyright 2021 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 @file:Suppress("NOTHING_TO_INLINE")
17 
18 package androidx.compose.ui.graphics
19 
20 import androidx.compose.ui.util.normalizedAngleCos
21 import androidx.compose.ui.util.normalizedAngleSin
22 
23 /**
24  * 4x5 matrix for transforming the color and alpha components of a source. The matrix can be passed
25  * as single array, and is treated as follows:
26  * ```
27  *  [ a, b, c, d, e,
28  *    f, g, h, i, j,
29  *    k, l, m, n, o,
30  *    p, q, r, s, t ]
31  * ```
32  *
33  * When applied to a color <code>[[R, G, B, A]]</code>, the resulting color is computed as:
34  * ```
35  *   R' = a*R + b*G + c*B + d*A + e;
36  *   G' = f*R + g*G + h*B + i*A + j;
37  *   B' = k*R + l*G + m*B + n*A + o;
38  *   A' = p*R + q*G + r*B + s*A + t;</pre>
39  *
40  * ```
41  *
42  * That resulting color <code>[[R', G', B', A']]</code> then has each channel clamped to the
43  * <code>0</code> to <code>255</code> range.
44  *
45  * The sample ColorMatrix below inverts incoming colors by scaling each channel by <code>-1</code>,
46  * and then shifting the result up by `255` to remain in the standard color space.
47  *
48  * ```
49  *   [ -1, 0, 0, 0, 255,
50  *     0, -1, 0, 0, 255,
51  *     0, 0, -1, 0, 255,
52  *     0, 0, 0, 1, 0 ]
53  * ```
54  *
55  * This is often used as input for [ColorFilter.colorMatrix] and applied at draw time through
56  * [Paint.colorFilter]
57  */
58 @kotlin.jvm.JvmInline
59 value class ColorMatrix(
60     val values: FloatArray =
61         floatArrayOf(1f, 0f, 0f, 0f, 0f, 0f, 1f, 0f, 0f, 0f, 0f, 0f, 1f, 0f, 0f, 0f, 0f, 0f, 1f, 0f)
62 ) {
63     // NOTE: This class contains a number of tests like this:
64     //
65     //     `if (values.size < 20) return`
66     //
67     // These tests exist to give the AOT compiler a hint about the size of the array.
68     // Their presence eliminates a large number of array bound checks. For instance,
69     // in the isIdentity method, this eliminates half of the instructions and half
70     // of the branches.
71     //
72     // These tests are not there to generate early returns (the test will always
73     // fail), but only to influence code generation.
74     //
75     // DO NOT REMOVE THOSE TESTS.
76 
77     /**
78      * Obtain an instance of the matrix value at the given [row] and [column]. [ColorMatrix] follows
79      * row major order in regards to the positions of matrix values within the flattened array. That
80      * is, content order goes from left to right then top to bottom as opposed to column major
81      * order.
82      *
83      * @param row Row index to query the ColorMatrix value. Range is from 0 to 3 as [ColorMatrix] is
84      *   represented as a 4 x 5 matrix
85      * @param column Column index to query the ColorMatrix value. Range is from 0 to 4 as
86      *   [ColorMatrix] is represented as a 4 x 5 matrix
87      */
88     inline operator fun get(row: Int, column: Int) = values[(row * 5) + column]
89 
90     /**
91      * Set the matrix value at the given [row] and [column]. [ColorMatrix] follows row major order
92      * in regards to the positions of matrix values within the flattened array. That is, content
93      * order goes from left to right then top to bottom as opposed to column major order.
94      *
95      * @param row Row index to query the ColorMatrix value. Range is from 0 to 3 as [ColorMatrix] is
96      *   represented as a 4 x 5 matrix
97      * @param column Column index to query the ColorMatrix value. Range is from 0 to 4 as
98      *   [ColorMatrix] is represented as a 4 x 5 matrix
99      * @param v value to update at the given [row] and [column]
100      */
101     inline operator fun set(row: Int, column: Int, v: Float) {
102         values[(row * 5) + column] = v
103     }
104 
105     /**
106      * Set this colormatrix to identity:
107      * ```
108      * [ 1 0 0 0 0   - red vector
109      * 0 1 0 0 0   - green vector
110      * 0 0 1 0 0   - blue vector
111      * 0 0 0 1 0 ] - alpha vector
112      * ```
113      */
114     inline fun reset() {
115         this[0, 0] = 1f
116         this[0, 1] = 0f
117         this[0, 2] = 0f
118         this[0, 3] = 0f
119         this[0, 4] = 0f
120 
121         this[1, 0] = 0f
122         this[1, 1] = 1f
123         this[1, 2] = 0f
124         this[1, 3] = 0f
125         this[1, 4] = 0f
126 
127         this[2, 0] = 0f
128         this[2, 1] = 0f
129         this[2, 2] = 1f
130         this[2, 3] = 0f
131         this[2, 4] = 0f
132 
133         this[3, 0] = 0f
134         this[3, 1] = 0f
135         this[3, 2] = 0f
136         this[3, 3] = 1f
137         this[3, 4] = 0f
138     }
139 
140     /** Assign the [src] colormatrix into this matrix, copying all of its values. */
141     fun set(src: ColorMatrix) {
142         val v1 = values
143         if (v1.size < 20) return
144 
145         val v2 = src.values
146         if (v2.size < 20) return
147 
148         v1[0] = v2[0]
149         v1[1] = v2[1]
150         v1[2] = v2[2]
151         v1[3] = v2[3]
152         v1[4] = v2[4]
153         v1[5] = v2[5]
154         v1[6] = v2[6]
155         v1[7] = v2[7]
156         v1[8] = v2[8]
157         v1[9] = v2[9]
158         v1[10] = v2[10]
159         v1[11] = v2[11]
160         v1[12] = v2[12]
161         v1[13] = v2[13]
162         v1[14] = v2[14]
163         v1[15] = v2[15]
164         v1[16] = v2[16]
165         v1[17] = v2[17]
166         v1[18] = v2[18]
167         v1[19] = v2[19]
168     }
169 
170     /**
171      * Internal helper method to handle rotation computation and provides a callback used to apply
172      * the result to different color rotation axes
173      */
174     private inline fun rotateInternal(degrees: Float, block: (cosine: Float, sine: Float) -> Unit) {
175         reset()
176         val normalizedAngle = degrees * (1.0f / 360.0f)
177         val cosine = normalizedAngleCos(normalizedAngle)
178         val sine = normalizedAngleSin(normalizedAngle)
179         block(cosine, sine)
180     }
181 
182     /** Multiply this matrix by [colorMatrix] and assign the result to this matrix. */
183     operator fun timesAssign(colorMatrix: ColorMatrix) {
184         if (values.size < 20) return
185 
186         val v00 = dot(this, 0, colorMatrix, 0)
187         val v01 = dot(this, 0, colorMatrix, 1)
188         val v02 = dot(this, 0, colorMatrix, 2)
189         val v03 = dot(this, 0, colorMatrix, 3)
190         val v04 =
191             this[0, 0] * colorMatrix[0, 4] +
192                 this[0, 1] * colorMatrix[1, 4] +
193                 this[0, 2] * colorMatrix[2, 4] +
194                 this[0, 3] * colorMatrix[3, 4] +
195                 this[0, 4]
196 
197         val v10 = dot(this, 1, colorMatrix, 0)
198         val v11 = dot(this, 1, colorMatrix, 1)
199         val v12 = dot(this, 1, colorMatrix, 2)
200         val v13 = dot(this, 1, colorMatrix, 3)
201         val v14 =
202             this[1, 0] * colorMatrix[0, 4] +
203                 this[1, 1] * colorMatrix[1, 4] +
204                 this[1, 2] * colorMatrix[2, 4] +
205                 this[1, 3] * colorMatrix[3, 4] +
206                 this[1, 4]
207 
208         val v20 = dot(this, 2, colorMatrix, 0)
209         val v21 = dot(this, 2, colorMatrix, 1)
210         val v22 = dot(this, 2, colorMatrix, 2)
211         val v23 = dot(this, 2, colorMatrix, 3)
212         val v24 =
213             this[2, 0] * colorMatrix[0, 4] +
214                 this[2, 1] * colorMatrix[1, 4] +
215                 this[2, 2] * colorMatrix[2, 4] +
216                 this[2, 3] * colorMatrix[3, 4] +
217                 this[2, 4]
218 
219         val v30 = dot(this, 3, colorMatrix, 0)
220         val v31 = dot(this, 3, colorMatrix, 1)
221         val v32 = dot(this, 3, colorMatrix, 2)
222         val v33 = dot(this, 3, colorMatrix, 3)
223         val v34 =
224             this[3, 0] * colorMatrix[0, 4] +
225                 this[3, 1] * colorMatrix[1, 4] +
226                 this[3, 2] * colorMatrix[2, 4] +
227                 this[3, 3] * colorMatrix[3, 4] +
228                 this[3, 4]
229 
230         this[0, 0] = v00
231         this[0, 1] = v01
232         this[0, 2] = v02
233         this[0, 3] = v03
234         this[0, 4] = v04
235         this[1, 0] = v10
236         this[1, 1] = v11
237         this[1, 2] = v12
238         this[1, 3] = v13
239         this[1, 4] = v14
240         this[2, 0] = v20
241         this[2, 1] = v21
242         this[2, 2] = v22
243         this[2, 3] = v23
244         this[2, 4] = v24
245         this[3, 0] = v30
246         this[3, 1] = v31
247         this[3, 2] = v32
248         this[3, 3] = v33
249         this[3, 4] = v34
250     }
251 
252     /**
253      * Set the matrix to affect the saturation of colors.
254      *
255      * @param sat A value of 0 maps the color to gray-scale. 1 is identity.
256      */
257     fun setToSaturation(sat: Float) {
258         if (values.size < 20) return
259         reset()
260 
261         val invSat = 1 - sat
262         val r = 0.213f * invSat
263         val g = 0.715f * invSat
264         val b = 0.072f * invSat
265 
266         this[0, 0] = r + sat
267         this[0, 1] = g
268         this[0, 2] = b
269         this[1, 0] = r
270         this[1, 1] = g + sat
271         this[1, 2] = b
272         this[2, 0] = r
273         this[2, 1] = g
274         this[2, 2] = b + sat
275     }
276 
277     /**
278      * Create a [ColorMatrix] with the corresponding scale parameters for the red, green, blue and
279      * alpha axes
280      *
281      * @param redScale Desired scale parameter for the red channel
282      * @param greenScale Desired scale parameter for the green channel
283      * @param blueScale Desired scale parameter for the blue channel
284      * @param alphaScale Desired scale parameter for the alpha channel
285      */
286     fun setToScale(redScale: Float, greenScale: Float, blueScale: Float, alphaScale: Float) {
287         if (values.size < 20) return
288         reset()
289 
290         this[0, 0] = redScale
291         this[1, 1] = greenScale
292         this[2, 2] = blueScale
293         this[3, 3] = alphaScale
294     }
295 
296     /** Rotate by [degrees] along the red color axis */
297     fun setToRotateRed(degrees: Float) {
298         if (values.size < 20) return
299 
300         rotateInternal(degrees) { cosine, sine ->
301             this[1, 1] = cosine
302             this[1, 2] = sine
303             this[2, 1] = -sine
304             this[2, 2] = cosine
305         }
306     }
307 
308     /** Rotate by [degrees] along the green color axis */
309     fun setToRotateGreen(degrees: Float) {
310         if (values.size < 20) return
311 
312         rotateInternal(degrees) { cosine, sine ->
313             this[0, 0] = cosine
314             this[0, 2] = -sine
315             this[2, 0] = sine
316             this[2, 2] = cosine
317         }
318     }
319 
320     /** Rotate by [degrees] along the blue color axis */
321     fun setToRotateBlue(degrees: Float) {
322         if (values.size < 20) return
323 
324         rotateInternal(degrees) { cosine, sine ->
325             this[0, 0] = cosine
326             this[0, 1] = sine
327             this[1, 0] = -sine
328             this[1, 1] = cosine
329         }
330     }
331 
332     /** Set the matrix to convert RGB to YUV */
333     fun convertRgbToYuv() {
334         if (values.size < 20) return
335         reset()
336 
337         // these coefficients match those in libjpeg
338         this[0, 0] = 0.299f
339         this[0, 1] = 0.587f
340         this[0, 2] = 0.114f
341         this[1, 0] = -0.16874f
342         this[1, 1] = -0.33126f
343         this[1, 2] = 0.5f
344         this[2, 0] = 0.5f
345         this[2, 1] = -0.41869f
346         this[2, 2] = -0.08131f
347     }
348 
349     /** Set the matrix to convert from YUV to RGB */
350     fun convertYuvToRgb() {
351         if (values.size < 20) return
352         reset()
353 
354         // these coefficients match those in libjpeg
355         this[0, 2] = 1.402f
356         this[1, 0] = 1f
357         this[1, 1] = -0.34414f
358         this[1, 2] = -0.71414f
359         this[2, 0] = 1f
360         this[2, 1] = 1.772f
361         this[2, 2] = 0f
362     }
363 }
364 
365 /**
366  * Helper method that returns the dot product of the top left 4 x 4 matrix of [ColorMatrix] used in
367  * [ColorMatrix.timesAssign]
368  */
dotnull369 private inline fun dot(m1: ColorMatrix, row: Int, m2: ColorMatrix, column: Int): Float {
370     return m1[row, 0] * m2[0, column] +
371         m1[row, 1] * m2[1, column] +
372         m1[row, 2] * m2[2, column] +
373         m1[row, 3] * m2[3, column]
374 }
375