All Downloads are FREE. Search and download functionalities are using the official Maven repository.

commonMain.utils.Color.kt Maven / Gradle / Ivy

/*
 * Copyright (c) 2024, Google LLC, OpenSavvy and contributors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package opensavvy.material3.colors.utils

import kotlin.jvm.JvmInline
import kotlin.math.pow
import kotlin.math.round

/**
 * Lightweight representation of a color.
 *
 * Internally, this color is represented in ARGB space.
 * Conversions to and from other color spaces are available as well.
 */
@JvmInline
value class Color(
	/**
	 * ARGB 32-bit representation of this color.
	 */
	val argb: Int,
) {

	// region ARGB accessors

	/**
	 * Transparency.
	 *
	 * From 0 (fully transparent) to 255 (fully opaque).
	 */
	val alpha: Int
		get() = argb shr 24 and 255

	/**
	 * Red component.
	 *
	 * From 0 (no red) to 255 (max red).
	 */
	val red: Int
		get() = argb shr 16 and 255

	/**
	 * Green component.
	 *
	 * From 0 (no green) to 255 (max green).
	 */
	val green: Int
		get() = argb shr 8 and 255

	/**
	 * Blue component.
	 *
	 * From 0 (no blue) to 255 (max blue).
	 */
	val blue: Int
		get() = argb and 255

	/**
	 * `true` is this color is fully opaque.
	 */
	val isOpaque: Boolean
		get() = alpha >= 255

	// endregion
	// region Converters

	/**
	 * Converts this color to the [XYZ color space](https://en.wikipedia.org/wiki/CIE_1931_color_space).
	 */
	fun toXyz(): DoubleArray {
		val r = linearized(red)
		val g = linearized(green)
		val b = linearized(blue)
		return matrixMultiply(doubleArrayOf(r, g, b), SRGB_TO_XYZ)
	}

	/**
	 * Converts this color to the [Lab color space](https://en.wikipedia.org/wiki/CIELAB_color_space).
	 */
	fun toLab(): DoubleArray {
		val linearR = linearized(red)
		val linearG = linearized(green)
		val linearB = linearized(blue)
		val matrix = SRGB_TO_XYZ
		val x = matrix[0][0] * linearR + matrix[0][1] * linearG + matrix[0][2] * linearB
		val y = matrix[1][0] * linearR + matrix[1][1] * linearG + matrix[1][2] * linearB
		val z = matrix[2][0] * linearR + matrix[2][1] * linearG + matrix[2][2] * linearB
		val whitePoint = whitePointD65
		val xNormalized = x / whitePoint[0]
		val yNormalized = y / whitePoint[1]
		val zNormalized = z / whitePoint[2]
		val fx = labF(xNormalized)
		val fy = labF(yNormalized)
		val fz = labF(zNormalized)
		val l = 116.0 * fy - 16
		val a = 500.0 * (fx - fy)
		val b = 200.0 * (fy - fz)
		return doubleArrayOf(l, a, b)
	}

	/**
	 * Converts to an L* value.
	 */
	fun toLstar(): Double {
		val y = toXyz()[1]
		return 116.0 * labF(y / 100.0) - 16.0
	}

	// endregion
	// region String representation

	private fun hexFromColor(color: Int): String {
		return color.toString(16)
			.padStart(2, '0')
	}

	override fun toString(): String =
		"#" + hexFromColor(red) + hexFromColor(green) + hexFromColor(blue)

	// endregion

	companion object {
		// region Converters

		fun fromRgb(red: Int, green: Int, blue: Int) = Color(
			argb = (255 shl 24) or ((red and 255) shl 16) or ((green and 255) shl 8) or (blue and 255)
		)

		/**
		 * Converts from linear RGB components.
		 *
		 * @param linrgb Must be an array of 3 values (RGB).
		 */
		fun fromLinrgb(linrgb: DoubleArray): Color {
			val r = delinearized(linrgb[0])
			val g = delinearized(linrgb[1])
			val b = delinearized(linrgb[2])
			return fromRgb(r, g, b)
		}

		/**
		 * Converts this color from the [XYZ color space](https://en.wikipedia.org/wiki/CIE_1931_color_space).
		 */
		fun fromXyz(x: Double, y: Double, z: Double): Color {
			val matrix = XYZ_TO_SRGB
			val linearR = matrix[0][0] * x + matrix[0][1] * y + matrix[0][2] * z
			val linearG = matrix[1][0] * x + matrix[1][1] * y + matrix[1][2] * z
			val linearB = matrix[2][0] * x + matrix[2][1] * y + matrix[2][2] * z
			val r = delinearized(linearR)
			val g = delinearized(linearG)
			val b = delinearized(linearB)
			return fromRgb(r, g, b)
		}

		/**
		 * Converts this color from the [Lab color space](https://en.wikipedia.org/wiki/CIELAB_color_space).
		 */
		fun fromLab(l: Double, a: Double, b: Double): Color {
			val whitePoint = whitePointD65
			val fy = (l + 16.0) / 116.0
			val fx = a / 500.0 + fy
			val fz = fy - b / 200.0
			val xNormalized = labInvf(fx)
			val yNormalized = labInvf(fy)
			val zNormalized = labInvf(fz)
			val x = xNormalized * whitePoint[0]
			val y = yNormalized * whitePoint[1]
			val z = zNormalized * whitePoint[2]
			return fromXyz(x, y, z)
		}

		/**
		 * Converts from an L* value.
		 */
		fun fromLstar(lstar: Double): Color {
			val y = yFromLstar(lstar)
			val component = delinearized(y)
			return fromRgb(component, component, component)
		}

		// endregion
		// region Y <-> L*


		/**
		 * Converts an L* value to a Y value.
		 *
		 *
		 * L* in L*a*b* and Y in XYZ measure the same quantity, luminance.
		 *
		 *
		 * L* measures perceptual luminance, a linear scale. Y in XYZ measures relative luminance, a
		 * logarithmic scale.
		 *
		 * @param lstar L* in L*a*b*
		 * @return Y in XYZ
		 */
		fun yFromLstar(lstar: Double): Double {
			return 100.0 * labInvf((lstar + 16.0) / 116.0)
		}

		/**
		 * Converts a Y value to an L* value.
		 *
		 *
		 * L* in L*a*b* and Y in XYZ measure the same quantity, luminance.
		 *
		 *
		 * L* measures perceptual luminance, a linear scale. Y in XYZ measures relative luminance, a
		 * logarithmic scale.
		 *
		 * @param y Y in XYZ
		 * @return L* in L*a*b*
		 */
		fun lstarFromY(y: Double): Double {
			return labF(y / 100.0) * 116.0 - 16.0
		}

		// endregion
		// region Well-known colors

		val BLACK = fromRgb(0, 0, 0)
		val WHITE = fromRgb(255, 255, 255)
		val RED = fromRgb(255, 0, 0)
		val GREEN = fromRgb(0, 255, 0)
		val BLUE = fromRgb(0, 0, 255)

		// endregion

		/**
		 * The standard white point; white on a sunny day.
		 */
		val whitePointD65: DoubleArray
			get() = doubleArrayOf(95.047, 100.0, 108.883)
	}
}

/**
 * Linearizes an RGB component.
 *
 * @param rgbComponent 0 <= rgb_component <= 255, represents R/G/B channel
 * @return 0.0 <= output <= 100.0, color channel converted to linear RGB space
 */
internal fun linearized(rgbComponent: Int): Double {
	val normalized = rgbComponent / 255.0
	return if (normalized <= 0.040449936) {
		normalized / 12.92 * 100.0
	} else {
		((normalized + 0.055) / 1.055).pow(2.4) * 100.0
	}
}

/**
 * Delinearizes an RGB component.
 *
 * @param rgbComponent 0.0 <= rgb_component <= 100.0, represents linear R/G/B channel
 * @return 0 <= output <= 255, color channel converted to regular RGB space
 */
internal fun delinearized(rgbComponent: Double): Int {
	val normalized = rgbComponent / 100.0
	var delinearized = 0.0
	delinearized = if (normalized <= 0.0031308) {
		normalized * 12.92
	} else {
		1.055 * normalized.pow(1.0 / 2.4) - 0.055
	}
	return clampInt(0, 255, round(delinearized * 255.0).toInt())
}

private val SRGB_TO_XYZ: Array = arrayOf(
	doubleArrayOf(0.41233895, 0.35762064, 0.18051042),
	doubleArrayOf(0.2126, 0.7152, 0.0722),
	doubleArrayOf(0.01932141, 0.11916382, 0.95034478),
)

private val XYZ_TO_SRGB: Array = arrayOf(
	doubleArrayOf(
		3.2413774792388685, -1.5376652402851851, -0.49885366846268053,
	),
	doubleArrayOf(
		-0.9691452513005321, 1.8758853451067872, 0.04156585616912061,
	),
	doubleArrayOf(
		0.05562093689691305, -0.20395524564742123, 1.0571799111220335,
	),
)

private fun labF(t: Double): Double {
	val e = 216.0 / 24389.0
	val kappa = 24389.0 / 27.0
	return if (t > e) {
		t.pow(1.0 / 3.0)
	} else {
		(kappa * t + 16) / 116
	}
}

private fun labInvf(ft: Double): Double {
	val e = 216.0 / 24389.0
	val kappa = 24389.0 / 27.0
	val ft3 = ft * ft * ft
	return if (ft3 > e) {
		ft3
	} else {
		(116 * ft - 16) / kappa
	}
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy