mirror of
https://gitlab.com/cc-ru/ocelot/ocelot-desktop.git
synced 2026-02-05 01:32:37 +01:00
graphics-v2: Add colors v2
This commit is contained in:
parent
1a526fe4ba
commit
2f311ad41f
@ -20,6 +20,7 @@ libraryDependencies += "org.scala-lang" % "scala-reflect" % scalaVersion.value
|
||||
|
||||
libraryDependencies += "org.scalatest" %% "scalatest" % "3.2.19" % "test"
|
||||
libraryDependencies += "org.scalatest" %% "scalatest-funsuite" % "3.2.19" % "test"
|
||||
libraryDependencies += "org.scalatestplus" %% "scalacheck-1-18" % "3.2.19.0" % "test"
|
||||
|
||||
libraryDependencies += "org.apache.logging.log4j" % "log4j-core" % "2.20.0"
|
||||
libraryDependencies += "org.apache.logging.log4j" % "log4j-api" % "2.20.0"
|
||||
|
||||
270
src/main/scala/ocelot/desktop/color_v2/Color.scala
Normal file
270
src/main/scala/ocelot/desktop/color_v2/Color.scala
Normal file
@ -0,0 +1,270 @@
|
||||
package ocelot.desktop.color_v2
|
||||
|
||||
import ocelot.desktop.color_v2.repr.PackedInt
|
||||
|
||||
import java.nio.ByteBuffer
|
||||
|
||||
/**
|
||||
* Linear-RGB with 8 bits for alpha and 16 bits per color channel, packed into a [[Long]].
|
||||
*
|
||||
* Has the following layout: A_RR_GG_BB.
|
||||
*
|
||||
* Uses premultiplied alpha (i.e. color channels are multiplied by alpha). This allows for accurate color blending
|
||||
* without color leaking from transparent areas. Additionally, there is a unique representation of a fully-transparent
|
||||
* color - that is (0, 0, 0, 0). Moreover, we get additive color blending for free - just set alpha to zero while
|
||||
* keeping color channels nonzero.
|
||||
*
|
||||
* The rationale for such packed encoding is to use a primitive type ([[AnyVal]]) to avoid allocations.
|
||||
*
|
||||
* Using 16 bits per color channel allows seamless sRGB/linear round-trips, assuming we only care about 8 bits per
|
||||
* channel in our images and on the screen. This doesn't quite hold when transcoding to formats which use
|
||||
* both straight alpha and non-linear sRGB, but that doesn't matter since during alpha blending this extra precision
|
||||
* would be lost anyway.
|
||||
*/
|
||||
case class Color(inner: Long) extends AnyVal {
|
||||
/** Red channel (0-65535). */
|
||||
def r16: Short = ((inner >> 32) & 0xFFFF).toShort
|
||||
|
||||
/** Green channel (0-65535). */
|
||||
def g16: Short = ((inner >> 16) & 0xFFFF).toShort
|
||||
|
||||
/** Blue channel (0-65535). */
|
||||
def b16: Short = (inner & 0xFFFF).toShort
|
||||
|
||||
/** Alpha channel (0-65535). */
|
||||
def a8: Byte = ((inner >> 48) & 0xFF).toByte
|
||||
|
||||
/** Checks whether two colors are close enough. */
|
||||
def closeTo(other: Color, colorEps: Int = 10, alphaEps: Int = 1): Boolean = {
|
||||
((r16.toInt & 0xFFFF) - (other.r16.toInt & 0xFFFF)).abs <= colorEps &&
|
||||
((g16.toInt & 0xFFFF) - (other.g16.toInt & 0xFFFF)).abs <= colorEps &&
|
||||
((b16.toInt & 0xFFFF) - (other.b16.toInt & 0xFFFF)).abs <= colorEps &&
|
||||
((a8.toInt & 0xFF) - (other.a8.toInt & 0xFF)).abs <= alphaEps
|
||||
}
|
||||
|
||||
/** Performs alpha blending against a background. */
|
||||
def blendOver(bg: Color): Color = {
|
||||
val fgR = r16.toInt & 0xFFFF
|
||||
val fgG = g16.toInt & 0xFFFF
|
||||
val fgB = b16.toInt & 0xFFFF
|
||||
val fgA = a8.toInt & 0xFF
|
||||
|
||||
val bgR = bg.r16.toInt & 0xFFFF
|
||||
val bgG = bg.g16.toInt & 0xFFFF
|
||||
val bgB = bg.b16.toInt & 0xFFFF
|
||||
val bgA = bg.a8.toInt & 0xFF
|
||||
|
||||
def alphaMul(r: Int, a: Int): Int = {
|
||||
// we want to compute: round((r / 0xFFFF) * (a / 0xFF) * 0xFFFF)
|
||||
// or in other words: round(r * a / 0xFF)
|
||||
// floor(r * a / 0xFF + 1 / 2)
|
||||
//
|
||||
// notice that AB / FF = 0.ABABAB (this holds for any other byte divided by 0xFF)
|
||||
// so we can replace division with multiplication by this repeated fraction
|
||||
//
|
||||
// we'll truncate this fraction to 24 bits and call it aaa.
|
||||
// similarly, this fraction to 16 bits will be called aa.
|
||||
//
|
||||
// we get: (r * aaa + 0x800000) / 2^24
|
||||
//
|
||||
// notice this multiplication doesn't fit into 32 bits (16 bits * 24 bits)
|
||||
// r * aaa = r * aa * 2^8 + ra
|
||||
// r * aaa = ra * 2^16 + ra * 2^8 + ra
|
||||
//
|
||||
// now we substitute
|
||||
// (ra * 2^16 + ra * 2^8 + ra + 0x800000) / 2^24
|
||||
// (ra * 2^8 + ra + ra / 2^8 + 0x8000) / 2^16
|
||||
|
||||
val ra = r * a
|
||||
(((ra << 8) + ra + (ra >> 8) + 0x8000) >> 16) & 0xFFFF
|
||||
}
|
||||
|
||||
val resR = fgR + alphaMul(bgR, 0xFF - fgA)
|
||||
val resG = fgG + alphaMul(bgG, 0xFF - fgA)
|
||||
val resB = fgB + alphaMul(bgB, 0xFF - fgA)
|
||||
val resA = fgA + alphaMul(bgA, 0xFF - fgA)
|
||||
|
||||
Color.pack(resR.toShort, resG.toShort, resB.toShort, resA.toByte)
|
||||
}
|
||||
}
|
||||
|
||||
object Color {
|
||||
val White: Color = PackedInt(0xFFFFFF).toColor
|
||||
val Grey: Color = PackedInt(0x808080).toColor
|
||||
val Black: Color = PackedInt(0x000000).toColor
|
||||
|
||||
val Red: Color = PackedInt(0xFF0000).toColor
|
||||
val Green: Color = PackedInt(0x00FF00).toColor
|
||||
val Blue: Color = PackedInt(0x0000FF).toColor
|
||||
|
||||
val Cyan: Color = PackedInt(0x00FFF).toColor
|
||||
val Magenta: Color = PackedInt(0xFF00FF).toColor
|
||||
val Yellow: Color = PackedInt(0xFFFF00).toColor
|
||||
|
||||
val Transparent: Color = Color(0)
|
||||
|
||||
def pack(r16: Short, g16: Short, b16: Short, a8: Byte): Color = {
|
||||
Color(
|
||||
((a8.toLong & 0xFF) << 48)
|
||||
| ((r16.toLong & 0xFFFF) << 32)
|
||||
| ((g16.toLong & 0xFFFF) << 16)
|
||||
| (b16.toLong & 0xFFFF)
|
||||
)
|
||||
}
|
||||
|
||||
/** Provides encoding of [[Color]] to bytes. */
|
||||
sealed abstract class Encoding {
|
||||
val size: Int
|
||||
|
||||
/** Encodes the given color to the destination buffer, advancing its position by [[size]]. */
|
||||
def encode(color: Color, dst: ByteBuffer): Unit
|
||||
|
||||
/** Encodes the given color, returning a byte array. */
|
||||
final def encodeToArray(color: Color): Array[Byte] = {
|
||||
val dst = ByteBuffer.allocate(size)
|
||||
encode(color, dst)
|
||||
dst.array()
|
||||
}
|
||||
|
||||
/** Decodes color from the source buffer, advancing its position by [[size]]. */
|
||||
def decode(src: ByteBuffer): Color
|
||||
|
||||
/** Decodes color from the provided byte array. */
|
||||
final def decodeFromArray(src: Array[Byte]): Color = {
|
||||
decode(ByteBuffer.wrap(src))
|
||||
}
|
||||
}
|
||||
|
||||
object Encoding {
|
||||
/** Linear encoding with a single 8-bit red channel. */
|
||||
case object LinearR8 extends Encoding {
|
||||
override val size: Int = 1
|
||||
|
||||
override def encode(color: Color, dst: ByteBuffer): Unit = {
|
||||
dst.put(shrink16To8Bits(color.r16))
|
||||
}
|
||||
|
||||
override def decode(src: ByteBuffer): Color = {
|
||||
val r = extend8To16Bits(src.get())
|
||||
pack(r, r, r, 0xFF.toByte)
|
||||
}
|
||||
}
|
||||
|
||||
/** Linear encoding with three 8-bit RGB channels. */
|
||||
case object LinearRgb8 extends Encoding {
|
||||
override val size: Int = 3
|
||||
|
||||
override def encode(color: Color, dst: ByteBuffer): Unit = {
|
||||
dst.put(shrink16To8Bits(color.r16))
|
||||
dst.put(shrink16To8Bits(color.g16))
|
||||
dst.put(shrink16To8Bits(color.b16))
|
||||
}
|
||||
|
||||
override def decode(src: ByteBuffer): Color = {
|
||||
val r = extend8To16Bits(src.get())
|
||||
val g = extend8To16Bits(src.get())
|
||||
val b = extend8To16Bits(src.get())
|
||||
pack(r, g, b, 0xFF.toByte)
|
||||
}
|
||||
}
|
||||
|
||||
/** Linear encoding with four 8-bit RGBA channels. */
|
||||
case object LinearRgba8 extends Encoding {
|
||||
override val size: Int = 4
|
||||
|
||||
override def encode(color: Color, dst: ByteBuffer): Unit = {
|
||||
dst.put(shrink16To8Bits(color.r16))
|
||||
dst.put(shrink16To8Bits(color.g16))
|
||||
dst.put(shrink16To8Bits(color.b16))
|
||||
dst.put(color.a8)
|
||||
}
|
||||
|
||||
override def decode(src: ByteBuffer): Color = {
|
||||
val r = extend8To16Bits(src.get())
|
||||
val g = extend8To16Bits(src.get())
|
||||
val b = extend8To16Bits(src.get())
|
||||
val a = src.get()
|
||||
pack(r, g, b, a)
|
||||
}
|
||||
}
|
||||
|
||||
/** Non-linear (sRGB) encoding with three 8-bit RGB channels. */
|
||||
case object SrgbRgb8 extends Encoding {
|
||||
override val size: Int = 3
|
||||
|
||||
override def encode(color: Color, dst: ByteBuffer): Unit = {
|
||||
dst.put(linear16ToSrgb8(color.r16))
|
||||
dst.put(linear16ToSrgb8(color.g16))
|
||||
dst.put(linear16ToSrgb8(color.b16))
|
||||
}
|
||||
|
||||
override def decode(src: ByteBuffer): Color = {
|
||||
val r = srgb8ToLinear16(src.get())
|
||||
val g = srgb8ToLinear16(src.get())
|
||||
val b = srgb8ToLinear16(src.get())
|
||||
pack(r, g, b, 0xFF.toByte)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/** Non-linear (sRGB) encoding with four 8-bit RGBA channels. */
|
||||
case object SrgbRgba8 extends Encoding {
|
||||
override val size: Int = 4
|
||||
|
||||
override def encode(color: Color, dst: ByteBuffer): Unit = {
|
||||
dst.put(linear16ToSrgb8(color.r16))
|
||||
dst.put(linear16ToSrgb8(color.g16))
|
||||
dst.put(linear16ToSrgb8(color.b16))
|
||||
dst.put(color.a8)
|
||||
}
|
||||
|
||||
override def decode(src: ByteBuffer): Color = {
|
||||
val r = srgb8ToLinear16(src.get())
|
||||
val g = srgb8ToLinear16(src.get())
|
||||
val b = srgb8ToLinear16(src.get())
|
||||
val a = src.get()
|
||||
pack(r, g, b, a)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Repeats a byte twice */
|
||||
private[color_v2] def extend8To16Bits(v: Byte): Short = (((v.toShort & 0xFF) << 8) | (v.toShort & 0xFF)).toShort
|
||||
|
||||
/** Returns high byte from 16-bit word */
|
||||
private[color_v2] def shrink16To8Bits(v: Short): Byte = (v >> 8).toByte
|
||||
|
||||
/** Converts non-linear sRGB color component (0-255) to linear color space (0-65535). */
|
||||
private[color_v2] def srgb8ToLinear16(v: Byte): Short = Srgb8ToLinear16Lut(v.toInt & 0xFF)
|
||||
|
||||
private lazy val Srgb8ToLinear16Lut: Array[Short] = {
|
||||
Array.tabulate(0x100) { i =>
|
||||
val srgb = i.toFloat / 0xFF.toFloat
|
||||
|
||||
val linear = if (srgb <= 0.04045) {
|
||||
srgb / 12.92
|
||||
} else {
|
||||
math.pow((srgb + 0.055) / 1.055, 2.4)
|
||||
}
|
||||
|
||||
(linear * 0xFFFF.toFloat).toShort
|
||||
}
|
||||
}
|
||||
|
||||
/** Converts linear color component (0-65535) to non-linear sRGB color space (0-255). */
|
||||
private[color_v2] def linear16ToSrgb8(v: Short): Byte = Linear16ToSrgb8Lut(v.toInt & 0xFFFF)
|
||||
|
||||
private lazy val Linear16ToSrgb8Lut: Array[Byte] = {
|
||||
Array.tabulate(0x10000) { i =>
|
||||
val linear = i.toFloat / 0xFFFF.toFloat
|
||||
|
||||
val srgb = if (linear <= 0.0031308) {
|
||||
linear * 12.92
|
||||
} else {
|
||||
1.055 * math.pow(linear, 1.0 / 2.4) - 0.055
|
||||
}
|
||||
|
||||
math.min((srgb * 0xFF.toFloat).round, 255.0).toInt.toByte
|
||||
}
|
||||
}
|
||||
}
|
||||
28
src/main/scala/ocelot/desktop/color_v2/repr/LinearRgba.scala
Normal file
28
src/main/scala/ocelot/desktop/color_v2/repr/LinearRgba.scala
Normal file
@ -0,0 +1,28 @@
|
||||
package ocelot.desktop.color_v2.repr
|
||||
|
||||
import ocelot.desktop.color_v2.Color
|
||||
import ocelot.desktop.color_v2.Color._
|
||||
|
||||
/** Linear RGBA color with double components (0-1) with premultiplied alpha. */
|
||||
case class LinearRgba(r: Double, g: Double, b: Double, a: Double) extends ReprInstance {
|
||||
override def toColor: Color = {
|
||||
pack((r * 0xFFFF).toShort, (g * 0xFFFF).toShort, (b * 0xFFFF).toShort, (a * 0XFF).toByte)
|
||||
}
|
||||
|
||||
def closeTo(other: LinearRgba, eps: Double = 1e-3): Boolean = {
|
||||
(r - other.r) <= eps && (g - other.g) <= eps && (b - other.b) <= eps && (a - other.a) <= eps
|
||||
}
|
||||
}
|
||||
|
||||
case object LinearRgba extends ReprObject {
|
||||
override type Instance = LinearRgba
|
||||
|
||||
override def fromColor(color: Color): LinearRgba = {
|
||||
LinearRgba(
|
||||
(color.r16.toInt & 0xFFFF).toDouble / 0xFFFF,
|
||||
(color.g16.toInt & 0xFFFF).toDouble / 0xFFFF,
|
||||
(color.b16.toInt & 0xFFFF).toDouble / 0xFFFF,
|
||||
(color.a8.toInt & 0xFF).toDouble / 0xFF
|
||||
)
|
||||
}
|
||||
}
|
||||
203
src/main/scala/ocelot/desktop/color_v2/repr/Okhsv.scala
Normal file
203
src/main/scala/ocelot/desktop/color_v2/repr/Okhsv.scala
Normal file
@ -0,0 +1,203 @@
|
||||
package ocelot.desktop.color_v2.repr
|
||||
|
||||
import ocelot.desktop.color_v2.Color
|
||||
|
||||
/** Color in Okhsv color space with premultiplied alpha.
|
||||
*
|
||||
* @param h Hue (0-360)
|
||||
* @param s Saturation (0-1)
|
||||
* @param v Value (0-1)
|
||||
* @param a Alpha (0-1)
|
||||
*/
|
||||
case class Okhsv(h: Double, s: Double, v: Double, a: Double) extends ReprInstance {
|
||||
override def toColor: Color = Okhsv.okhsvToOklav(this).toColor
|
||||
|
||||
def closeTo(other: Okhsv, eps: Double = 1e-3): Boolean = {
|
||||
def angleDiff(angle1: Double, angle2: Double): Double = {
|
||||
val diff = (angle2 - angle1).abs
|
||||
if (diff > 180) 360 - diff else diff
|
||||
}
|
||||
|
||||
angleDiff(h, other.h) <= eps * 360 &&
|
||||
(s - other.s).abs <= eps &&
|
||||
(v - other.v).abs <= eps &&
|
||||
(a - other.a).abs <= eps
|
||||
}
|
||||
}
|
||||
|
||||
case object Okhsv extends ReprObject {
|
||||
override type Instance = Okhsv
|
||||
|
||||
override def fromColor(color: Color): Okhsv = Okhsv.oklabToOkhsv(Oklab.fromColor(color))
|
||||
|
||||
// Code below is translated from JS taken from
|
||||
// https://github.com/bottosson/bottosson.github.io/blob/master/misc/colorpicker/colorconversion.js
|
||||
//
|
||||
// Copyright (c) 2021 Björn Ottosson
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
// this software and associated documentation files (the "Software"), to deal in
|
||||
// the Software without restriction, including without limitation the rights to
|
||||
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
// of the Software, and to permit persons to whom the Software is furnished to do
|
||||
// so, subject to the following conditions:
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
private def toe(x: Double): Double = {
|
||||
val k1 = 0.206
|
||||
val k2 = 0.03
|
||||
val k3 = (1 + k1) / (1 + k2)
|
||||
0.5 * (k3 * x - k1 + math.sqrt((k3 * x - k1) * (k3 * x - k1) + 4 * k2 * k3 * x))
|
||||
}
|
||||
|
||||
private def toeInv(x: Double): Double = {
|
||||
val k1 = 0.206
|
||||
val k2 = 0.03
|
||||
val k3 = (1 + k1) / (1 + k2)
|
||||
(x * x + k1 * x) / (k3 * (x + k2))
|
||||
}
|
||||
|
||||
private def computeMaxSaturation(a: Double, b: Double): Double = {
|
||||
val (k0, k1, k2, k3, k4, wl, wm, ws) =
|
||||
if (-1.88170328 * a - 0.80936493 * b > 1) {
|
||||
// Red component
|
||||
(1.19086277, 1.76576728, 0.59662641, 0.75515197, 0.56771245,
|
||||
4.0767416621, -3.3077115913, 0.2309699292)
|
||||
} else if (1.81444104 * a - 1.19445276 * b > 1) {
|
||||
// Green component
|
||||
(0.73956515, -0.45954404, 0.08285427, 0.12541070, 0.14503204,
|
||||
-1.2684380046, 2.6097574011, -0.3413193965)
|
||||
} else {
|
||||
// Blue component
|
||||
(1.35733652, -0.00915799, -1.15130210, -0.50559606, 0.00692167,
|
||||
-0.0041960863, -0.7034186147, 1.7076147010)
|
||||
}
|
||||
|
||||
var S = k0 + k1 * a + k2 * b + k3 * a * a + k4 * a * b
|
||||
|
||||
val kl = +0.3963377774 * a + 0.2158037573 * b
|
||||
val km = -0.1055613458 * a - 0.0638541728 * b
|
||||
val ks = -0.0894841775 * a - 1.2914855480 * b
|
||||
|
||||
{
|
||||
val l_ = 1 + S * kl
|
||||
val m_ = 1 + S * km
|
||||
val s_ = 1 + S * ks
|
||||
|
||||
val l = l_ * l_ * l_
|
||||
val m = m_ * m_ * m_
|
||||
val s = s_ * s_ * s_
|
||||
|
||||
val l_dS = 3 * kl * l_ * l_
|
||||
val m_dS = 3 * km * m_ * m_
|
||||
val s_dS = 3 * ks * s_ * s_
|
||||
|
||||
val l_dS2 = 6 * kl * kl * l_
|
||||
val m_dS2 = 6 * km * km * m_
|
||||
val s_dS2 = 6 * ks * ks * s_
|
||||
|
||||
val f = wl * l + wm * m + ws * s
|
||||
val f1 = wl * l_dS + wm * m_dS + ws * s_dS
|
||||
val f2 = wl * l_dS2 + wm * m_dS2 + ws * s_dS2
|
||||
|
||||
S -= f * f1 / (f1 * f1 - 0.5 * f * f2)
|
||||
}
|
||||
|
||||
S
|
||||
}
|
||||
|
||||
private def findCusp(a: Double, b: Double): (Double, Double) = {
|
||||
val S_cusp = computeMaxSaturation(a, b)
|
||||
val rgbAtMax = Oklab(1, S_cusp * a, S_cusp * b, 1).toLinearRgba
|
||||
val L_cusp = math.cbrt(1 / math.max(math.max(rgbAtMax.r, rgbAtMax.g), rgbAtMax.b))
|
||||
val C_cusp = L_cusp * S_cusp
|
||||
(L_cusp, C_cusp)
|
||||
}
|
||||
|
||||
private def getSTMax(a: Double, b: Double): (Double, Double) = {
|
||||
val cusp = findCusp(a, b)
|
||||
val L = cusp._1
|
||||
val C = cusp._2
|
||||
(C / L, C / (1 - L))
|
||||
}
|
||||
|
||||
private def okhsvToOklav(hsv: Okhsv): Oklab = {
|
||||
val h = hsv.h / 360.0
|
||||
val s = hsv.s
|
||||
val v = hsv.v
|
||||
|
||||
val a_ = math.cos(2 * math.Pi * h)
|
||||
val b_ = math.sin(2 * math.Pi * h)
|
||||
|
||||
val ST_max = getSTMax(a_, b_)
|
||||
val S_max = ST_max._1
|
||||
val T = ST_max._2
|
||||
val S_0 = 0.5
|
||||
val k = 1 - S_0 / S_max
|
||||
|
||||
val L_v = 1 - s * S_0 / (S_0 + T - T * k * s)
|
||||
val C_v = s * T * S_0 / (S_0 + T - T * k * s)
|
||||
|
||||
var L = v * L_v
|
||||
var C = v * C_v
|
||||
|
||||
val L_vt = toeInv(L_v)
|
||||
val C_vt = C_v * L_vt / L_v
|
||||
|
||||
val L_new = toeInv(L)
|
||||
C = C * L_new / L
|
||||
L = L_new
|
||||
|
||||
val rgbScale = Oklab(L_vt, a_ * C_vt, b_ * C_vt, 1).toLinearRgba
|
||||
val scaleL = math.cbrt(1 / math.max(math.max(rgbScale.r, rgbScale.g), rgbScale.b))
|
||||
|
||||
L = L * scaleL
|
||||
C = C * scaleL
|
||||
|
||||
Oklab(L, C * a_, C * b_, hsv.a)
|
||||
}
|
||||
|
||||
private def oklabToOkhsv(lab: Oklab): Okhsv = {
|
||||
var C = math.sqrt(lab.colorA * lab.colorA + lab.colorB * lab.colorB)
|
||||
val a_ = lab.colorA / C
|
||||
val b_ = lab.colorB / C
|
||||
|
||||
var L = lab.lightness
|
||||
val h = 0.5 + 0.5 * math.atan2(-lab.colorB, -lab.colorA) / math.Pi
|
||||
|
||||
val ST_max = getSTMax(a_, b_)
|
||||
val S_max = ST_max._1
|
||||
val T = ST_max._2
|
||||
val S_0 = 0.5
|
||||
val k = 1 - S_0 / S_max
|
||||
|
||||
val t = T / (C + L * T)
|
||||
val L_v = t * L
|
||||
val C_v = t * C
|
||||
|
||||
val L_vt = toeInv(L_v)
|
||||
val C_vt = C_v * L_vt / L_v
|
||||
|
||||
val rgbScale = Oklab(L_vt, a_ * C_vt, b_ * C_vt, 1).toLinearRgba
|
||||
val scaleL = math.cbrt(1f / math.max(math.max(rgbScale.r, rgbScale.g), rgbScale.b))
|
||||
|
||||
L = L / scaleL
|
||||
C = C / scaleL
|
||||
|
||||
C = C * toe(L) / L
|
||||
L = toe(L)
|
||||
|
||||
val v = L / L_v
|
||||
val s = (S_0 + T) * C_v / ((T * S_0) + T * k * C_v)
|
||||
|
||||
Okhsv(h * 360.0, s, v, lab.alpha)
|
||||
}
|
||||
}
|
||||
54
src/main/scala/ocelot/desktop/color_v2/repr/Oklab.scala
Normal file
54
src/main/scala/ocelot/desktop/color_v2/repr/Oklab.scala
Normal file
@ -0,0 +1,54 @@
|
||||
package ocelot.desktop.color_v2.repr
|
||||
|
||||
import ocelot.desktop.color_v2.Color
|
||||
|
||||
/** Color in Oklab color space with premultiplied alpha.
|
||||
*
|
||||
* @param lightness Perceived lightness.
|
||||
* @param colorA How green/red the color is.
|
||||
* @param colorB How blue/yellow the color is.
|
||||
* @param alpha Alpha (0-1)
|
||||
*/
|
||||
case class Oklab(lightness: Double, colorA: Double, colorB: Double, alpha: Double) extends ReprInstance {
|
||||
private[repr] def toLinearRgba: LinearRgba = {
|
||||
val l_ = +0.2158037573 * colorB + 0.3963377774 * colorA + lightness
|
||||
val m_ = -0.0638541728 * colorB - 0.1055613458 * colorA + lightness
|
||||
val s_ = -1.2914855480 * colorB - 0.0894841775 * colorA + lightness
|
||||
val l = l_ * l_ * l_
|
||||
val m = m_ * m_ * m_
|
||||
val s = s_ * s_ * s_
|
||||
LinearRgba(
|
||||
r = +0.2309699292 * s + 4.0767416621 * l -3.3077115913 * m,
|
||||
g = -0.3413193965 * s + -1.2684380046 * l +2.6097574011 * m,
|
||||
b = +1.7076147010 * s + -0.0041960863 * l -0.7034186147 * m,
|
||||
a = alpha
|
||||
)
|
||||
}
|
||||
|
||||
override def toColor: Color = toLinearRgba.toColor
|
||||
|
||||
def closeTo(other: Oklab, eps: Double = 1e-3): Boolean = {
|
||||
(lightness - other.lightness) <= eps &&
|
||||
(colorA - other.colorA) <= eps &&
|
||||
(colorB - other.colorB) <= eps &&
|
||||
(alpha - other.alpha) <= eps
|
||||
}
|
||||
}
|
||||
|
||||
case object Oklab extends ReprObject {
|
||||
override type Instance = Oklab
|
||||
|
||||
private[repr] def fromLinearRgba(c: LinearRgba): Oklab = {
|
||||
val l = math.cbrt(0.0514459929 * c.b + 0.4122214708 * c.r + 0.5363325363 * c.g)
|
||||
val m = math.cbrt(0.1073969566 * c.b + 0.2119034982 * c.r + 0.6806995451 * c.g)
|
||||
val s = math.cbrt(0.6299787005 * c.b + 0.0883024619 * c.r + 0.2817188376 * c.g)
|
||||
Oklab(
|
||||
lightness = -0.0040720468 * s + 0.2104542553 * l + 0.7936177850 * m,
|
||||
colorA = +0.4505937099 * s + 1.9779984951 * l - 2.4285922050 * m,
|
||||
colorB = -0.8086757660 * s + 0.0259040371 * l + 0.7827717662 * m,
|
||||
alpha = c.a
|
||||
)
|
||||
}
|
||||
|
||||
override def fromColor(color: Color): Oklab = fromLinearRgba(LinearRgba.fromColor(color))
|
||||
}
|
||||
31
src/main/scala/ocelot/desktop/color_v2/repr/PackedInt.scala
Normal file
31
src/main/scala/ocelot/desktop/color_v2/repr/PackedInt.scala
Normal file
@ -0,0 +1,31 @@
|
||||
package ocelot.desktop.color_v2.repr
|
||||
|
||||
import ocelot.desktop.color_v2.Color
|
||||
import ocelot.desktop.color_v2.Color._
|
||||
|
||||
/** sRGB color packed into an [[Int]] (0xRRGGBB). No alpha. Very common in OpenComputers world. */
|
||||
case class PackedInt(inner: Int) extends AnyVal with ReprInstance {
|
||||
/** Red channel (0-255). */
|
||||
def r: Byte = ((inner >> 16) & 0xFF).toByte
|
||||
|
||||
/** Green channel (0-255). */
|
||||
def g: Byte = ((inner >> 8) & 0xFF).toByte
|
||||
|
||||
/** Blue channel (0-255). */
|
||||
def b: Byte = (inner & 0xFF).toByte
|
||||
|
||||
override def toColor: Color = {
|
||||
pack(srgb8ToLinear16(r), srgb8ToLinear16(g), srgb8ToLinear16(b), 0xFF.toByte)
|
||||
}
|
||||
}
|
||||
|
||||
case object PackedInt extends ReprObject {
|
||||
override type Instance = PackedInt
|
||||
|
||||
override def fromColor(color: Color): PackedInt = {
|
||||
val r = linear16ToSrgb8(color.r16).toInt & 0xFF
|
||||
val g = linear16ToSrgb8(color.g16).toInt & 0xFF
|
||||
val b = linear16ToSrgb8(color.b16).toInt & 0xFF
|
||||
PackedInt((r << 16) | (g << 8) | b)
|
||||
}
|
||||
}
|
||||
18
src/main/scala/ocelot/desktop/color_v2/repr/package.scala
Normal file
18
src/main/scala/ocelot/desktop/color_v2/repr/package.scala
Normal file
@ -0,0 +1,18 @@
|
||||
package ocelot.desktop.color_v2
|
||||
|
||||
package object repr {
|
||||
/** Provides encoding and decoding of various alternative color representations to [[Color]]. */
|
||||
trait ReprObject {
|
||||
/** Type of the alternative representation */
|
||||
type Instance <: ReprInstance;
|
||||
|
||||
/** Converts [[Color]] to the alternative representation */
|
||||
def fromColor(color: Color): Instance;
|
||||
}
|
||||
|
||||
/** Provides encoding and decoding of various alternative color representations to [[Color]]. */
|
||||
trait ReprInstance extends Any {
|
||||
/** Converts this representation to a [[Color]]. */
|
||||
def toColor: Color
|
||||
}
|
||||
}
|
||||
@ -37,7 +37,7 @@ trait Graphics {
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a static image with given dimensions, format, and data (packed without alignment or padding).
|
||||
* Creates a static image with given dimensions, format, and data (row-major, packed without alignment or padding).
|
||||
*
|
||||
* @throws IllegalArgumentException if width or height is zero or negative,
|
||||
* if data is too short or too long for given format and dimensions
|
||||
|
||||
@ -44,22 +44,56 @@ object Image {
|
||||
}
|
||||
|
||||
/** Image formats */
|
||||
sealed class Format
|
||||
abstract sealed class Format {
|
||||
/** Returns true if the format is linear (has no gamma-correction). */
|
||||
def isLinear: Boolean
|
||||
|
||||
/** Returns the number of bytes per pixel assuming packed encoding. */
|
||||
def bytesPerPixel: Int
|
||||
}
|
||||
|
||||
object Format {
|
||||
/** Linear format with one R 8-bit channel */
|
||||
case object LinearR8 extends Format
|
||||
case object LinearR8 extends Format {
|
||||
override val isLinear = true
|
||||
|
||||
override def bytesPerPixel: Int = 1
|
||||
}
|
||||
|
||||
/** Linear format with three RGB 8-bit channels */
|
||||
case object LinearRgb8 extends Format
|
||||
case object LinearRgb8 extends Format {
|
||||
override val isLinear = true
|
||||
|
||||
/** Linear format with four RGBA 8-bit channels */
|
||||
case object LinearRgba8 extends Format
|
||||
override def bytesPerPixel: Int = 3
|
||||
}
|
||||
|
||||
/**
|
||||
* Linear format with four RGBA 8-bit channels.
|
||||
*
|
||||
* Color is NOT premultiplied by alpha.
|
||||
*/
|
||||
case object LinearRgba8 extends Format {
|
||||
override val isLinear = true
|
||||
|
||||
override def bytesPerPixel: Int = 4
|
||||
}
|
||||
|
||||
/** Gamma-corrected (nonlinear sRGB) format with three RGB 8-bit channels */
|
||||
case object SrgbRgb8 extends Format
|
||||
case object SrgbRgb8 extends Format {
|
||||
override val isLinear = false
|
||||
|
||||
/** Gamma-corrected (nonlinear sRGB) format with four RGBA 8-bit channels (alpha is still linear) */
|
||||
case object SrgbRgba8 extends Format
|
||||
override def bytesPerPixel: Int = 3
|
||||
}
|
||||
|
||||
/**
|
||||
* Gamma-corrected (nonlinear sRGB) format with four RGBA 8-bit channels.
|
||||
*
|
||||
* Alpha is still linear. Color is NOT premultiplied by alpha.
|
||||
*/
|
||||
case object SrgbRgba8 extends Format {
|
||||
override val isLinear = false
|
||||
|
||||
override def bytesPerPixel: Int = 4
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,47 @@
|
||||
package ocelot.desktop.graphics_v2.reference_impl
|
||||
|
||||
import ocelot.desktop.geometry.Size2D
|
||||
import ocelot.desktop.graphics_v2.{Graphics, Image}
|
||||
|
||||
import java.nio.ByteBuffer
|
||||
|
||||
class GraphicsImpl extends Graphics {
|
||||
override type StaticImage = GraphicsImpl.StaticImageImpl
|
||||
override type Surface = GraphicsImpl.SurfaceImpl
|
||||
|
||||
override def createStaticImage(width: Int, height: Int, format: Image.Format, packedData: ByteBuffer): StaticImage = {
|
||||
new StaticImage(this, new ImageBuffer(width, height, format, packedData))
|
||||
}
|
||||
|
||||
override def destroyStaticImage(image: StaticImage): Unit = {
|
||||
require(image.owner == this, "StaticImage doesn't belong to this Graphics instance")
|
||||
require(!image.isDestroyed, "StaticImage is already destroyed")
|
||||
image.isDestroyed = true
|
||||
}
|
||||
|
||||
override def createSurface(width: Int, height: Int, format: Image.Format): Surface = {
|
||||
new Surface(this, new ImageBuffer(width, height, format))
|
||||
}
|
||||
|
||||
override def destroySurface(surface: Surface): Unit = {
|
||||
require(surface.owner == this, "Surface doesn't belong to this Graphics instance")
|
||||
require(!surface.isDestroyed, "Surface is already destroyed")
|
||||
surface.isDestroyed = true
|
||||
}
|
||||
|
||||
override def process(passes: Array[Pass]): Unit = ???
|
||||
}
|
||||
|
||||
object GraphicsImpl {
|
||||
class StaticImageImpl private[reference_impl](val owner: GraphicsImpl, val imageBuffer: ImageBuffer) extends Image.Static {
|
||||
private[reference_impl] var isDestroyed = false
|
||||
|
||||
override def size: Size2D = imageBuffer.size
|
||||
}
|
||||
|
||||
class SurfaceImpl private[reference_impl](val owner: GraphicsImpl, val imageBuffer: ImageBuffer) extends Image.Surface {
|
||||
private[reference_impl] var isDestroyed = false
|
||||
|
||||
override def size: Size2D = imageBuffer.size
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,26 @@
|
||||
package ocelot.desktop.graphics_v2.reference_impl
|
||||
|
||||
import ocelot.desktop.geometry.Size2D
|
||||
import ocelot.desktop.graphics_v2.Image
|
||||
|
||||
import java.nio.ByteBuffer
|
||||
|
||||
private[reference_impl] class ImageBuffer(val width: Int, val height: Int, val format: Image.Format, val data: ByteBuffer) {
|
||||
require(width * height * format.bytesPerPixel == data.limit(), "Wrong ImageBuffer data size");
|
||||
|
||||
def this(width: Int, height: Int, format: Image.Format) {
|
||||
this(width, height, format, ByteBuffer.allocateDirect(width * height * format.bytesPerPixel))
|
||||
}
|
||||
|
||||
def size: Size2D = Size2D(width, height)
|
||||
|
||||
def getPixel(x: Int, y: Int): PackedColor = {
|
||||
val offset = (y * width + x) * format.bytesPerPixel
|
||||
PackedColor.decode(format, offset, data)
|
||||
}
|
||||
|
||||
def putPixel(x: Int, y: Int, color: PackedColor): Unit = {
|
||||
val offset = (y * width + x) * format.bytesPerPixel
|
||||
color.encode(format, offset, data)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,144 @@
|
||||
package ocelot.desktop.graphics_v2.reference_impl
|
||||
|
||||
import ocelot.desktop.graphics_v2.Image
|
||||
import ocelot.desktop.graphics_v2.Image.Format
|
||||
|
||||
import java.nio.ByteBuffer
|
||||
|
||||
/**
|
||||
* Packed linear color with premultiplied color with 8 bits for alpha and 16 bits per color channel.
|
||||
*
|
||||
* Has the following layout: A_RR_GG_BB.
|
||||
*
|
||||
* The rationale for such encoding is to use a primitive type ([[AnyVal]]) to avoid allocations.
|
||||
*
|
||||
* Using 16 bits per color channel allows seamless sRGB/linear and premultiplied/not-premultiplied round-trips,
|
||||
* assuming we only store 8 bits per channel in our images.
|
||||
*/
|
||||
private[reference_impl] case class PackedColor(packed: Long) extends AnyVal {
|
||||
def r16: Short = ((packed >> 32) & 0xFFFF).toShort
|
||||
|
||||
def g16: Short = ((packed >> 16) & 0xFFFF).toShort
|
||||
|
||||
def b16: Short = (packed & 0xFFFF).toShort
|
||||
|
||||
def a8: Byte = ((packed >> 48) & 0xFF).toByte
|
||||
|
||||
def encode(format: Image.Format, offset: Int, data: ByteBuffer): Unit = {
|
||||
PackedColor.encode(format, offset, data, this)
|
||||
}
|
||||
}
|
||||
|
||||
private[reference_impl] object PackedColor {
|
||||
private def pack(r: Short, g: Short, b: Short, a: Short): PackedColor = {
|
||||
PackedColor(((a & 0xFF) << 40) | ((r & 0xFFFF) << 32) | ((g & 0xFFFF) << 16) | (b & 0xFFFF))
|
||||
}
|
||||
|
||||
private def extend8To16Bits(v: Byte): Short = ((v << 8) | v).toShort
|
||||
|
||||
private def shrink16To8Bits(v: Short): Byte = (v / 0xFF).toByte
|
||||
|
||||
private def preMultiply(col: Short, alpha: Byte): Short = (col * alpha / 0xFF).toShort
|
||||
|
||||
private def unPreMultiply(col: Short, alpha: Byte): Short = if (alpha == 0) 0 else (col * 0xFF / alpha).toShort
|
||||
|
||||
private val Srgb8ToLinear16Lut: Array[Short] = {
|
||||
(0 to 0xFF).map { i =>
|
||||
val srgb = i.toFloat / 0xFF.toFloat
|
||||
|
||||
val linear = if (srgb <= 0.04045) {
|
||||
srgb / 12.92
|
||||
} else {
|
||||
math.pow((srgb + 0.055) / 1.055, 2.4)
|
||||
}
|
||||
|
||||
(linear * 0xFFFF.toFloat).toShort
|
||||
}.toArray
|
||||
}
|
||||
|
||||
private def srgb8ToLinear16(v: Byte): Short = Srgb8ToLinear16Lut(v)
|
||||
|
||||
private val Linear16ToSrgb8Lut: Array[Byte] = {
|
||||
(0 to 0xFFFF).map { i =>
|
||||
val linear = i.toFloat / 0xFFFF.toFloat
|
||||
|
||||
val srgb = if (linear <= 0.0031308) {
|
||||
linear * 12.92
|
||||
} else {
|
||||
1.055 * math.pow(linear, 1.0/2.4) - 0.055
|
||||
}
|
||||
|
||||
(srgb * 0xFF.toFloat).toByte
|
||||
}.toArray
|
||||
}
|
||||
|
||||
private def linear16ToSrgb8(v: Short): Byte = Linear16ToSrgb8Lut(v)
|
||||
|
||||
def decode(format: Image.Format, offset: Int, data: ByteBuffer): PackedColor = {
|
||||
format match {
|
||||
case Format.LinearR8 =>
|
||||
val r = extend8To16Bits(data.get(offset))
|
||||
pack(r, r, r, 0xFF)
|
||||
|
||||
case Format.LinearRgb8 =>
|
||||
val r = extend8To16Bits(data.get(offset + 0))
|
||||
val g = extend8To16Bits(data.get(offset + 1))
|
||||
val b = extend8To16Bits(data.get(offset + 2))
|
||||
pack(r, g, b, 0xFF)
|
||||
|
||||
case Format.LinearRgba8 =>
|
||||
val r = extend8To16Bits(data.get(offset + 0))
|
||||
val g = extend8To16Bits(data.get(offset + 1))
|
||||
val b = extend8To16Bits(data.get(offset + 2))
|
||||
val a = data.get(offset + 3)
|
||||
pack(r, g, b, a)
|
||||
|
||||
case Format.SrgbRgb8 =>
|
||||
val r = srgb8ToLinear16(data.get(offset + 0))
|
||||
val g = srgb8ToLinear16(data.get(offset + 1))
|
||||
val b = srgb8ToLinear16(data.get(offset + 2))
|
||||
pack(r, g, b, 0xFF)
|
||||
|
||||
case Format.SrgbRgba8 =>
|
||||
val a = data.get(offset + 3)
|
||||
val r = preMultiply(srgb8ToLinear16(data.get(offset + 0)), a)
|
||||
val g = preMultiply(srgb8ToLinear16(data.get(offset + 1)), a)
|
||||
val b = preMultiply(srgb8ToLinear16(data.get(offset + 2)), a)
|
||||
pack(r, g, b, a)
|
||||
}
|
||||
}
|
||||
|
||||
private def encode(format: Image.Format, offset: Int, data: ByteBuffer, color: PackedColor): Unit = {
|
||||
val r = color.r16
|
||||
val g = color.g16
|
||||
val b = color.b16
|
||||
val a = color.a8
|
||||
|
||||
format match {
|
||||
case Format.LinearR8 =>
|
||||
data.put(offset, shrink16To8Bits(r))
|
||||
|
||||
case Format.LinearRgb8 =>
|
||||
data.put(offset + 0, shrink16To8Bits(r))
|
||||
data.put(offset + 1, shrink16To8Bits(g))
|
||||
data.put(offset + 2, shrink16To8Bits(b))
|
||||
|
||||
case Format.LinearRgba8 =>
|
||||
data.put(offset + 0, shrink16To8Bits(r))
|
||||
data.put(offset + 1, shrink16To8Bits(g))
|
||||
data.put(offset + 2, shrink16To8Bits(b))
|
||||
data.put(offset + 3, a)
|
||||
|
||||
case Format.SrgbRgb8 =>
|
||||
data.put(offset + 0, linear16ToSrgb8(r))
|
||||
data.put(offset + 1, linear16ToSrgb8(g))
|
||||
data.put(offset + 2, linear16ToSrgb8(b))
|
||||
|
||||
case Format.SrgbRgba8 =>
|
||||
data.put(offset + 0, linear16ToSrgb8(unPreMultiply(r, a)))
|
||||
data.put(offset + 1, linear16ToSrgb8(unPreMultiply(g, a)))
|
||||
data.put(offset + 2, linear16ToSrgb8(unPreMultiply(b, a)))
|
||||
data.put(offset + 3, a)
|
||||
}
|
||||
}
|
||||
}
|
||||
109
src/test/scala/ocelot/desktop/color_v2/ColorTest.scala
Normal file
109
src/test/scala/ocelot/desktop/color_v2/ColorTest.scala
Normal file
@ -0,0 +1,109 @@
|
||||
package ocelot.desktop.color_v2
|
||||
|
||||
import ocelot.desktop.color_v2.Color.Encoding
|
||||
import ocelot.desktop.util.UnitTest
|
||||
|
||||
class ColorTest extends UnitTest {
|
||||
test("Pack-unpack round-trip") {
|
||||
forAll { (r16: Short, g16: Short, b16: Short, a8: Byte) =>
|
||||
val packed = Color.pack(r16, g16, b16, a8)
|
||||
assert(packed.r16 == r16)
|
||||
assert(packed.g16 == g16)
|
||||
assert(packed.b16 == b16)
|
||||
assert(packed.a8 == a8)
|
||||
}
|
||||
}
|
||||
|
||||
def checkEncodingRoundTrip(encoding: Encoding, src: Array[Byte]): Unit = {
|
||||
val packed = encoding.decodeFromArray(src)
|
||||
val decoded = encoding.encodeToArray(packed)
|
||||
assert(src sameElements decoded)
|
||||
}
|
||||
|
||||
def checkEncoding(encoding: Encoding, src: Array[Byte], expectedEncoding: Long): Unit = {
|
||||
checkEncodingRoundTrip(encoding, src)
|
||||
val packed = encoding.decodeFromArray(src)
|
||||
assert(packed.inner == expectedEncoding)
|
||||
}
|
||||
|
||||
test("LinearR8 encoding examples") {
|
||||
checkEncoding(Encoding.LinearR8, Array(0x00.toByte), 0xFF_0000_0000_0000L)
|
||||
checkEncoding(Encoding.LinearR8, Array(0x13.toByte), 0xFF_1313_1313_1313L)
|
||||
checkEncoding(Encoding.LinearR8, Array(0xFF.toByte), 0xFF_FFFF_FFFF_FFFFL)
|
||||
}
|
||||
|
||||
test("LinearR8 encoding round-trip") {
|
||||
forAll { (r: Byte) =>
|
||||
checkEncodingRoundTrip(Encoding.LinearR8, Array(r))
|
||||
}
|
||||
}
|
||||
|
||||
test("LinearRgb8 encoding examples") {
|
||||
checkEncoding(Encoding.LinearRgb8, Array(0x00.toByte, 0x00.toByte, 0x00.toByte), 0xFF_0000_0000_0000L)
|
||||
checkEncoding(Encoding.LinearRgb8, Array(0x12.toByte, 0x34.toByte, 0x56.toByte), 0xFF_1212_3434_5656L)
|
||||
checkEncoding(Encoding.LinearRgb8, Array(0xFF.toByte, 0xFF.toByte, 0xFF.toByte), 0xFF_FFFF_FFFF_FFFFL)
|
||||
}
|
||||
|
||||
test("LinearRgb8 encoding round-trip") {
|
||||
forAll { (r: Byte, g: Byte, b: Byte) =>
|
||||
checkEncodingRoundTrip(Encoding.LinearRgb8, Array(r, g, b))
|
||||
}
|
||||
}
|
||||
|
||||
test("SrgbRgb8 encoding examples") {
|
||||
checkEncoding(Encoding.SrgbRgb8, Array(0x00.toByte, 0x00.toByte, 0x00.toByte), 0xFF_0000_0000_0000L)
|
||||
checkEncoding(Encoding.SrgbRgb8, Array(0x12.toByte, 0x34.toByte, 0x56.toByte), 0xFF_018C_08Ca_17D2L)
|
||||
checkEncoding(Encoding.SrgbRgb8, Array(0xFF.toByte, 0xFF.toByte, 0xFF.toByte), 0xFF_FFFF_FFFF_FFFFL)
|
||||
}
|
||||
|
||||
test("SrgbRgb8 round-trip") {
|
||||
forAll { (r: Byte, g: Byte, b: Byte) =>
|
||||
checkEncodingRoundTrip(Encoding.SrgbRgb8, Array(r, g, b))
|
||||
}
|
||||
}
|
||||
|
||||
test("LinearRgba8 encoding examples") {
|
||||
checkEncoding(Encoding.LinearRgba8, Array(0x00.toByte, 0x00.toByte, 0x00.toByte, 0x00.toByte), 0x00_0000_0000_0000L)
|
||||
checkEncoding(Encoding.LinearRgba8, Array(0x12.toByte, 0x34.toByte, 0x56.toByte, 0x42.toByte), 0x42_1212_3434_5656L)
|
||||
checkEncoding(Encoding.LinearRgba8, Array(0xFF.toByte, 0xFF.toByte, 0xFF.toByte, 0xFF.toByte), 0xFF_FFFF_FFFF_FFFFL)
|
||||
}
|
||||
|
||||
test("LinearRgba8 encoding round-trip") {
|
||||
forAll { (r: Byte, g: Byte, b: Byte, a: Byte) =>
|
||||
checkEncodingRoundTrip(Encoding.LinearRgba8, Array(r, g, b, a))
|
||||
}
|
||||
}
|
||||
|
||||
test("SrgbRgba8 encoding examples") {
|
||||
checkEncoding(Encoding.SrgbRgba8, Array(0x00.toByte, 0x00.toByte, 0x00.toByte, 0x00.toByte), 0x00_0000_0000_0000L)
|
||||
checkEncoding(Encoding.SrgbRgba8, Array(0x12.toByte, 0x34.toByte, 0x56.toByte, 0x42.toByte), 0x42_018C_08Ca_17D2L)
|
||||
checkEncoding(Encoding.SrgbRgba8, Array(0xFF.toByte, 0xFF.toByte, 0xFF.toByte, 0xFF.toByte), 0xFF_FFFF_FFFF_FFFFL)
|
||||
}
|
||||
|
||||
test("SrgbRgba8 encoding round-trip") {
|
||||
forAll { (r: Byte, g: Byte, b: Byte, a: Byte) =>
|
||||
checkEncodingRoundTrip(Encoding.SrgbRgba8, Array(r, g, b, a))
|
||||
}
|
||||
}
|
||||
|
||||
def checkAlphaBlending(bg: Long, fg: Long, res: Long): Unit = {
|
||||
val blend = Color(fg).blendOver(Color(bg))
|
||||
assert(blend.inner == res)
|
||||
}
|
||||
|
||||
test("Alpha blending (opaque foreground)") {
|
||||
checkAlphaBlending(0xFF_1234_5678_9ABCL, 0xFF_CBA9_8765_4321L, 0xFF_CBA9_8765_4321L);
|
||||
}
|
||||
|
||||
test("Alpha blending (white bg + 33% black fg)") {
|
||||
checkAlphaBlending(0xFF_FFFF_FFFF_FFFFL, 0x55_0000_0000_0000L, 0xFF_AAAA_AAAA_AAAAL);
|
||||
}
|
||||
|
||||
test("Alpha blending (white bg, 50% black fg)") {
|
||||
checkAlphaBlending(0xFF_FFFF_FFFF_FFFFL, 0x80_0000_0000_0000L, 0xFF_7F7F_7F7F_7F7FL);
|
||||
}
|
||||
|
||||
test("Alpha blending (33% red bg, 66% cyan fg)") {
|
||||
checkAlphaBlending(0x55_5555_0000_0000L, 0xAA_0000_AAAA_AAAAL, 0xC6_1C72_AAAA_AAAAL);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,27 @@
|
||||
package ocelot.desktop.color_v2.repr
|
||||
|
||||
import ocelot.desktop.color_v2.Color
|
||||
import ocelot.desktop.util.UnitTest
|
||||
|
||||
class LinearRgbaTest extends UnitTest {
|
||||
def checkConversion(rgba: LinearRgba, packed: Long): Unit = {
|
||||
assert(rgba.toColor.inner == packed)
|
||||
assert(LinearRgba.fromColor(Color(packed)).closeTo(rgba))
|
||||
}
|
||||
|
||||
test("Pure red") {
|
||||
checkConversion(LinearRgba(1.0, 0.0, 0.0, 1.0), 0xFF_FFFF_0000_0000L)
|
||||
}
|
||||
|
||||
test("Pure green") {
|
||||
checkConversion(LinearRgba(0.0, 1.0, 0.0, 1.0), 0xFF_0000_FFFF_0000L)
|
||||
}
|
||||
|
||||
test("Pure blue") {
|
||||
checkConversion(LinearRgba(0.0, 0.0, 1.0, 1.0), 0xFF_0000_0000_FFFFL)
|
||||
}
|
||||
|
||||
test("33% red, 66% blue, 33% alpha") {
|
||||
checkConversion(LinearRgba(1.0 / 3.0, 0.0, 2.0 / 3.0, 1.0 / 3.0), 0x55_5555_0000_AAAAL)
|
||||
}
|
||||
}
|
||||
27
src/test/scala/ocelot/desktop/color_v2/repr/OkhsvTest.scala
Normal file
27
src/test/scala/ocelot/desktop/color_v2/repr/OkhsvTest.scala
Normal file
@ -0,0 +1,27 @@
|
||||
package ocelot.desktop.color_v2.repr
|
||||
|
||||
import ocelot.desktop.color_v2.Color
|
||||
import ocelot.desktop.util.UnitTest
|
||||
|
||||
class OkhsvTest extends UnitTest {
|
||||
def checkConversion(okhsv: Okhsv, packed: Long): Unit = {
|
||||
assert(okhsv.toColor.closeTo(Color(packed)))
|
||||
assert(Okhsv.fromColor(Color(packed)).closeTo(okhsv))
|
||||
}
|
||||
|
||||
test("Pure red") {
|
||||
checkConversion(Okhsv(29.23388519234263, 0.9995219692256989, 1.0000000001685625, 1), 0xFF_FFFF_0000_0000L)
|
||||
}
|
||||
|
||||
test("Pure green") {
|
||||
checkConversion(Okhsv(142.49533888780996, 0.9999997210415695, 0.9999999884428648, 1.0), 0xFF_0000_FFFF_0000L)
|
||||
}
|
||||
|
||||
test("Pure blue") {
|
||||
checkConversion(Okhsv(264.052020638055, 0.9999910912349018, 0.9999999646150918, 1), 0xFF_0000_0000_FFFFL)
|
||||
}
|
||||
|
||||
test("33% red, 66% blue, 33% alpha") {
|
||||
checkConversion(Okhsv(311.9937528323292, 1.0005489476783134, 0.8409729279819962, 1.0 / 3.0), 0x55_5555_0000_AAAAL)
|
||||
}
|
||||
}
|
||||
27
src/test/scala/ocelot/desktop/color_v2/repr/OklabTest.scala
Normal file
27
src/test/scala/ocelot/desktop/color_v2/repr/OklabTest.scala
Normal file
@ -0,0 +1,27 @@
|
||||
package ocelot.desktop.color_v2.repr
|
||||
|
||||
import ocelot.desktop.color_v2.Color
|
||||
import ocelot.desktop.util.UnitTest
|
||||
|
||||
class OklabTest extends UnitTest {
|
||||
def checkConversion(oklab: Oklab, packed: Long): Unit = {
|
||||
assert(oklab.toColor.closeTo(Color(packed)))
|
||||
assert(Oklab.fromColor(Color(packed)).closeTo(oklab))
|
||||
}
|
||||
|
||||
test("Pure red") {
|
||||
checkConversion(Oklab(0.6279553606145516, 0.22486306106597398, 0.1258462985307351, 1), 0xFF_FFFF_0000_0000L)
|
||||
}
|
||||
|
||||
test("Pure green") {
|
||||
checkConversion(Oklab(0.8664396115356694, -0.23388757418790818, 0.17949847989672985, 1.0), 0xFF_0000_FFFF_0000L)
|
||||
}
|
||||
|
||||
test("Pure blue") {
|
||||
checkConversion(Oklab(0.4520137183853429, -0.03245698416876397, -0.3115281476783751, 1), 0xFF_0000_0000_FFFFL)
|
||||
}
|
||||
|
||||
test("33% red, 66% blue, 33% alpha") {
|
||||
checkConversion(Oklab(0.5281181319927706, 0.176826580031683, -0.19642887916863233, 1f / 3f), 0x55_5555_0000_AAAAL)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,27 @@
|
||||
package ocelot.desktop.color_v2.repr
|
||||
|
||||
import ocelot.desktop.color_v2.Color
|
||||
import ocelot.desktop.util.UnitTest
|
||||
|
||||
class PackedIntTest extends UnitTest {
|
||||
def checkConversion(packedInt: PackedInt, packed: Long): Unit = {
|
||||
assert(packedInt.toColor.inner == packed)
|
||||
assert(PackedInt.fromColor(Color(packed)) == packedInt)
|
||||
}
|
||||
|
||||
test("Pure red") {
|
||||
checkConversion(PackedInt(0xFF0000), 0xFF_FFFF_0000_0000L)
|
||||
}
|
||||
|
||||
test("Pure green") {
|
||||
checkConversion(PackedInt(0x00FF00), 0xFF_0000_FFFF_0000L)
|
||||
}
|
||||
|
||||
test("Pure blue") {
|
||||
checkConversion(PackedInt(0x0000FF), 0xFF_0000_0000_FFFFL)
|
||||
}
|
||||
|
||||
test("33% red, 66% blue") {
|
||||
checkConversion(PackedInt(0x5500AA), 0xFF_1741_0000_66E7L)
|
||||
}
|
||||
}
|
||||
@ -1,5 +1,6 @@
|
||||
package ocelot.desktop.util
|
||||
|
||||
import org.scalatest.funsuite.AnyFunSuite
|
||||
import org.scalatestplus.scalacheck.ScalaCheckPropertyChecks
|
||||
|
||||
trait UnitTest extends AnyFunSuite
|
||||
trait UnitTest extends AnyFunSuite with ScalaCheckPropertyChecks
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user