Skip to content

Commit

Permalink
Add hex, hexa, rgb, and rgba interpolators
Browse files Browse the repository at this point in the history
  • Loading branch information
dariusj authored and davesmith00000 committed Dec 18, 2024
1 parent 33bcf8f commit a61041e
Show file tree
Hide file tree
Showing 4 changed files with 225 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -1236,6 +1236,56 @@ class CreateShaderAST[Q <: Quotes](using val qq: Q) extends ShaderMacroUtils:
case _ =>
throw ShaderError.Unsupported("Shaders do not support infix operator: " + op)

case Apply(
Apply(Ident(op), List(Apply(_, List(Typed(Repeated(List(Literal(StringConstant(value))), _), _))))),
_
) =>
op match
case "hex" =>
import ultraviolet.syntax.interpolators.hex.*
value.toVec3.map(v => ShaderAST.DataTypes.vec3(v.x, v.y, v.z)).getOrElse {
throw ShaderError.Unsupported(
"Hex values must be 6 or 8 characters long using the `hex` or `hexa` interpolators, respectively (e.g. #FF00FF)."
)
}

case "hexa" =>
import ultraviolet.syntax.interpolators.hex.*
value.toVec4.map(v => ShaderAST.DataTypes.vec4(v.x, v.y, v.z, v.a)).getOrElse {
throw ShaderError.Unsupported(
"Hex values must be 6 or 8 characters long using the `hex` or `hexa` interpolators, respectively (e.g. #FF00FF00)."
)
}

case "rgb" =>
import ultraviolet.syntax.interpolators.rgb.*
value.toVec3
.map(v => ShaderAST.DataTypes.vec3(v.x, v.y, v.z))
.getOrElse {
throw ShaderError.Unsupported(
"RGB values must be 3 or 4 integers long using the `rgb` or `rgba` interpolators, respectively (e.g. 255,0,255)."
)
}

case "rgba" =>
import ultraviolet.syntax.interpolators.rgb.*
value.toVec4
.map(v => ShaderAST.DataTypes.vec4(v.x, v.y, v.z, v.a))
.getOrElse {
throw ShaderError.Unsupported(
"RGB values must be 3 or 4 integers long using the `rgb` or `rgba` interpolators, respectively (e.g. 255,0,255,0)."
)
}

case _ =>
throw ShaderError.Unsupported("Shaders do not support interpolators of type: " + op)

case Apply(
Apply(Ident(op), List(Apply(_, List(Typed(Repeated(_, _), _))))),
_
) =>
throw ShaderError.Unsupported("Shader interpolated colours must be string literals, e.g. #ff0000")

case Apply(Apply(Ident(op), List(l)), List(r)) =>
op match
case "<" | "<=" | ">" | ">=" | "==" | "!=" =>
Expand Down
73 changes: 73 additions & 0 deletions ultraviolet/shared/src/main/scala/ultraviolet/syntax.scala
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import ultraviolet.macros.UBOReader
import scala.annotation.StaticAnnotation
import scala.annotation.nowarn
import scala.deriving.Mirror
import scala.util.matching.Regex

object syntax extends ShaderDSLOps:
type WebGL1 = ultraviolet.datatypes.ShaderPrinter.WebGL1
Expand Down Expand Up @@ -91,4 +92,76 @@ object syntax extends ShaderDSLOps:
def PrecisionMediumPFloat: ShaderHeader = ShaderHeader.PrecisionMediumPFloat
def PrecisionLowPFloat: ShaderHeader = ShaderHeader.PrecisionLowPFloat

private[ultraviolet] object interpolators:

object hex:
private val hexGroup: String = "([0-9A-F]{2})"
private val hex3: Regex = List.fill(3)(hexGroup).mkString("(?i)#", "", "").r
private val hex4: Regex = List.fill(4)(hexGroup).mkString("(?i)#", "", "").r

private def toScaledFloat(string: String): Float = Integer.parseInt(string, 16) / 255f

extension (string: String) {
def toVec3: Option[vec3] = Option(string).collect { case hex3(r, g, b) =>
vec3(toScaledFloat(r), toScaledFloat(g), toScaledFloat(b))
}

def toVec4: Option[vec4] = Option(string).collect { case hex4(r, g, b, a) =>
vec4(toScaledFloat(r), toScaledFloat(g), toScaledFloat(b), toScaledFloat(a))
}
}

object rgb:
private def is8bit(i: Int): Boolean = i >= 0 && i < 256
private def toScaledFloat(string: String): Option[Float] = string.toIntOption.filter(is8bit).map(_ / 255f)

extension (string: String) {
def toVec3: Option[vec3] = Option(string.split(",").toList.map(toScaledFloat)).collect {
case Some(r) :: Some(g) :: Some(b) :: Nil => vec3(r, g, b)
}

def toVec4: Option[vec4] = Option(string.split(",").toList.map(toScaledFloat)).collect {
case Some(r) :: Some(g) :: Some(b) :: Some(a) :: Nil => vec4(r, g, b, a)
}
}

extension (sc: StringContext) {

@SuppressWarnings(Array("scalafix:DisableSyntax.throw"))
def hex(args: Any*): vec3 =
import interpolators.hex.*
sc.s(args*).toVec3.getOrElse {
throw IllegalArgumentException(
s"Invalid hex values ${args.mkString}. Supported formats are #00ff00 and #00ff00ff (case insensitive), using the 'hex' and 'hexa' interpolators, respectively"
)
}

@SuppressWarnings(Array("scalafix:DisableSyntax.throw"))
def hexa(args: Any*): vec4 =
import interpolators.hex.*
sc.s(args*).toVec4.getOrElse {
throw IllegalArgumentException(
s"Invalid hexa values ${args.mkString}. Supported formats are #00ff00 and #00ff00ff (case insensitive), using the 'hex' and 'hexa' interpolators, respectively"
)
}

@SuppressWarnings(Array("scalafix:DisableSyntax.throw"))
def rgb(args: Int*): vec3 =
import interpolators.rgb.*
sc.s(args*).toVec3.getOrElse {
throw IllegalArgumentException(
s"Invalid rgb values ${args.mkString}. Supported formats are 0,255,0 and 0,255,0,255, using the 'rgb' and 'rgba' interpolators, respectively"
)
}

@SuppressWarnings(Array("scalafix:DisableSyntax.throw"))
def rgba(args: Int*): vec4 =
import interpolators.rgb.*
sc.s(args*).toVec4.getOrElse {
throw IllegalArgumentException(
s"Invalid rgba values ${args.mkString}. Supported formats are 0,255,0 and 0,255,0,255, using the 'rgb' and 'rgba' interpolators, respectively"
)
}
}

end syntax
56 changes: 56 additions & 0 deletions ultraviolet/shared/src/test/scala/ultraviolet/SyntaxTests.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package ultraviolet

import scala.util.Random

import syntax.*

class SyntaxTests extends munit.FunSuite {

test("hex interpolator") {
assertEquals(hex"#00FF00", vec3(0f, 1f, 0f))
assertEquals(hex"#ff00ff", vec3(1f, 0f, 1f))
val (hex1, hex2, hex3) = ("00", "ff", "00")
assertEquals(hex"#$hex1$hex2$hex3", vec3(0f, 1f, 0f))

intercept[IllegalArgumentException](hex"#00000"): Unit
intercept[IllegalArgumentException](hex"#0000000"): Unit
intercept[IllegalArgumentException](hex"#gggggg"): Unit
}

test("hexa interpolator") {
assertEquals(hexa"#00FF00FF", vec4(0f, 1f, 0f, 1f))
assertEquals(hexa"#ff00ff00", vec4(1f, 0f, 1f, 0f))
val (hex1, hex2, hex3, hex4) = ("00", "ff", "00", "ff")
assertEquals(hexa"#$hex1$hex2$hex3$hex4", vec4(0f, 1f, 0f, 1f))

intercept[IllegalArgumentException](hexa"#0000000"): Unit
intercept[IllegalArgumentException](hexa"#000000000"): Unit
intercept[IllegalArgumentException](hexa"#gggggggg"): Unit
}

test("rgb interpolator") {
assertEquals(rgb"0,0,0", vec3(0f, 0f, 0f))
assertEquals(rgb"255,0,255", vec3(1f, 0f, 1f))
val (int1, int2, int3) = (0, 255, 0)
assertEquals(rgb"$int1,$int2,$int3", vec3(0f, 1f, 0f))

intercept[IllegalArgumentException](rgb"0,0"): Unit
intercept[IllegalArgumentException](rgb"0,0,0,0"): Unit
intercept[IllegalArgumentException](rgb"0, 0, 0"): Unit
intercept[IllegalArgumentException](rgb"-1,0,0"): Unit
intercept[IllegalArgumentException](rgb"256,0,0"): Unit
}

test("rgba interpolator") {
assertEquals(rgba"0,0,0,0", vec4(0f, 0f, 0f, 0f))
assertEquals(rgba"255,0,255,0", vec4(1f, 0f, 1f, 0f))
val (int1, int2, int3, int4) = (0, 255, 0, 255)
assertEquals(rgba"$int1,$int2,$int3,$int4", vec4(0f, 1f, 0f, 1f))

intercept[IllegalArgumentException](rgba"0,0,0"): Unit
intercept[IllegalArgumentException](rgba"0,0,0,0,0"): Unit
intercept[IllegalArgumentException](rgba"0, 0, 0, 0"): Unit
intercept[IllegalArgumentException](rgba"-1,0,0,0"): Unit
intercept[IllegalArgumentException](rgba"256,0,0,0"): Unit
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package ultraviolet.acceptance

import ultraviolet.DebugAST
import ultraviolet.syntax.*

class GLSLInterpolatorTests extends munit.FunSuite {
test("hex interpolator") {
inline def fragment: Shader[Unit, vec4] = Shader(_ => vec4(hex"#ff00ff", 0f))

// println(DebugAST.toAST(fragment))

val actual = fragment.toGLSL[WebGL2].toOutput.code

assertEquals(actual, "vec4(vec3(1.0,0.0,1.0),0.0);")
}

test("hexa interpolator") {
inline def fragment: Shader[Unit, vec4] = Shader(_ => hexa"#ff00ff00")

// println(DebugAST.toAST(fragment))

val actual = fragment.toGLSL[WebGL2].toOutput.code

assertEquals(actual, "vec4(1.0,0.0,1.0,0.0);")
}

test("rgb interpolator") {
inline def fragment: Shader[Unit, vec4] = Shader(_ => vec4(rgb"255,0,255", 0f))

// println(DebugAST.toAST(fragment))

val actual = fragment.toGLSL[WebGL2].toOutput.code

assertEquals(actual, "vec4(vec3(1.0,0.0,1.0),0.0);")
}

test("rgba interpolator") {
inline def fragment: Shader[Unit, vec4] = Shader(_ => rgba"255,0,255,0")

// println(DebugAST.toAST(fragment))

val actual = fragment.toGLSL[WebGL2].toOutput.code

assertEquals(actual, "vec4(1.0,0.0,1.0,0.0);")
}
}

0 comments on commit a61041e

Please sign in to comment.