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