diff --git a/binstream.effekt b/binstream.effekt new file mode 100644 index 000000000..45a604fbd --- /dev/null +++ b/binstream.effekt @@ -0,0 +1,403 @@ +import option +import stringbuffer +import stream +import char +import test +import io/error +import bytearray + +// assumes by default: +// BE byteorder, BE bitorder, unsigned + +// Wrappers +// -------- +record BE[A](raw: A) +record LE[A](raw: A) +record OfWidth[A](raw: A, width: Int) +record Signed[A](raw: A) + +/// Bits +type Bit { B0(); B1() } + +/// Effect alias +effect HexSplices = { + splice[Char], splice[String], + splice[Unit], + splice[Int], + splice[Byte], + splice[BE[Int]], splice[LE[Int]], + splice[LE[Signed[Int]]], splice[OfWidth[LE[Int]]], splice[OfWidth[LE[Signed[Int]]]], + splice[BE[Signed[Int]]], splice[OfWidth[BE[Int]]], splice[OfWidth[BE[Signed[Int]]]], + splice[ByteArray] +} + +// Splitting +// --------- +def bytesLE(int: Int, w: Int): Unit / emit[Byte] = { + var c = int + repeat(w){ + do emit(mod(c, 256).toByte) + c = c / 256 + } +} +def bytesLE(int: Int): Unit / emit[Byte] = bytesLE(int, 4) +def bytesBE(n: Int, width: Int): Unit / emit[Byte] = { + var pos = pow(256, width - 1) + repeat(width){ + do emit((bitwiseAnd(n, pos * 255) / pos).toByte) + pos = pos / 256 + } +} +def bytesBE(n: Int): Unit / emit[Byte] = bytesBE(n, 4) +def bytes(n: Int): Unit / emit[Byte] = bytesBE(n) +def signedBytesLE(int: Int, width: Int): Unit / emit[Byte] = { + if (int < 0) { + bytesLE(bitwiseNot(neg(int)) + 1, width) + } else { + bytesLE(int, width) + } +} +def signedBytesBE(int: Int, width: Int): Unit / emit[Byte] = { + if (int < 0) { + bytesBE(bitwiseNot(neg(int)) + 1, width) + } else { + bytesBE(int, width) + } +} +def signedBytesLE(int: Int): Unit / emit[Byte] = signedBytesLE(int, 4) +def signedBytesBE(int: Int): Unit / emit[Byte] = signedBytesBE(int, 4) +def bitsBE(int: Int): Unit / emit[Bit] = bitsBE(int, 32) +def collectBitsBE{ body: => Unit / emit[Bit] }: Int = { + var res = 0 + try body() with emit[Bit] { b => + res = b match { + case B0() => res * 2 + case B1() => res * 2 + 1 + } + resume(()) + } + res +} +def not(b: Bit): Bit = b match { + case B0() => B1() + case B1() => B0() +} +def bitwiseNot(n: Int): Int = { + collectBitsBE{ + try bitsBE(n) with emit[Bit]{ b => resume(do emit(not(b))) } + } +} + +// Splicers +// -------- + +def hex{ body: => Unit / { literal, HexSplices } }: Unit / emit[Byte] = { + try { + try { + body() + } + with splice[String] { s => + feed(s){ exhaustively{ do splice[Char](do read[Char]()) } } + resume(()) + } + with splice[ByteArray] { ba => + ba.foreach{ b => do splice[Byte](b) } + resume(()) + } + } + with literal { s => + feed(s){ + exhaustively { + with on[MissingValue].default{ () } + val upper: Int = hexDigitValue(do read[Char]()).value + val lower: Int = hexDigitValue(do read[Char]()).value + do emit[Byte]((16 * upper + lower).toByte) + } + } + resume(()) + } + with splice[Char] { c => do emit[Byte](c.toInt.toByte); resume(()) } + with splice[Byte] { b => do emit(b); resume(()) } + with splice[Unit] { u => resume(()) } + with splice[Int] { n => bytesBE(n); resume(()) } + with splice[LE[Int]] { w => bytesLE(w.raw); resume(()) } + with splice[BE[Int]] { v => bytesBE(v.raw); resume(()) } + with splice[LE[Signed[Int]]] { w => signedBytesLE(w.raw.raw); resume(()) } + with splice[OfWidth[LE[Int]]] { w => bytesLE(w.raw.raw, w.width); resume(()) } + with splice[OfWidth[LE[Signed[Int]]]] { w => signedBytesLE(w.raw.raw.raw, w.width); resume(()) } + with splice[BE[Signed[Int]]] { w => signedBytesBE(w.raw.raw); resume(()) } + with splice[OfWidth[BE[Int]]] { w => bytesBE(w.raw.raw, w.width); resume(()) } + with splice[OfWidth[BE[Signed[Int]]]] { w => signedBytesBE(w.raw.raw.raw, w.width); resume(()) } +} + +def x{ body: => Unit / { literal, HexSplices } }: Int = { + var res = 0 + for[Byte]{ hex{body} }{ v => res = res * 256 + v.toInt } + res +} + +// Counting and padding +// -------------------- +effect pad[A](fac: Int){ gen: => A }: Unit +effect getPos(): Int +def tracking[A](init: Int){ body: => Unit / { emit[A], getPos, pad[A] } }: Unit / emit[A] = { + var n = init + try body() + with emit[A] { b => n = n + 1; resume(do emit[A](b)) } + with getPos{ resume(n) } + with pad[A] { fac => + resume { {gen} => + while(mod(n, fac) != 0){ + do emit[A](gen()) + n = n + 1 + } + } + } +} +def tracking[A]{ body: => Unit / { emit[A], getPos, pad[A] } }: Unit / emit[A] = + tracking[A](0){body} + +// Sub-Byte +// ======== + +// From/to Bytes +// ------------- +def bitsLE(byte: Byte): Unit / emit[Bit] = { + val v = byte.toInt + var mask = 1 + repeat(8){ + bitwiseAnd(v, mask) match { + case 0 => do emit(B0()) + case _ => do emit(B1()) + } + mask = mask * 2 + } +} +def bitsBE(byte: Byte): Unit / emit[Bit] = { + val v = byte.toInt + var mask = 128 + repeat(8){ + bitwiseAnd(v, mask) match { + case 0 => do emit(B0()) + case _ => do emit(B1()) + } + mask = mask / 2 + } +} +def bits(byte: Byte): Unit / emit[Bit] = bitsBE(byte) +def bitsLE(v: Int, width: Int): Unit / emit[Bit] = { + var mask = 1 + repeat(width){ + bitwiseAnd(v, mask) match { + case 0 => do emit(B0()) + case _ => do emit(B1()) + } + mask = mask * 2 + } +} +def pow(n: Int, exp: Int): Int = { + def go(n: Int, exp: Int, acc: Int): Int = { + if (exp == 0) { + acc + } else if (mod(exp, 2) == 0) { + go(n * n, exp / 2, acc) + } else { + go(n * n, exp / 2, acc * n) + } + } + go(n, exp, 1) +} +def bitsBE(v: Int, width: Int): Unit / emit[Bit] = { + var mask = pow(2, width - 1) + repeat(width){ + bitwiseAnd(v, mask) match { + case 0 => do emit(B0()) + case _ => do emit(B1()) + } + mask = mask / 2 + } +} +def ungroupBytes{ body: => Unit / emit[Byte] }: Unit / emit[Bit] = + for[Byte]{body}{ b => bits(b) } +def twoscomplementLE{ body: => Unit / emit[Bit] }: Unit / emit[Bit] = { + var carry = true + try body() + with emit[Bit] { + case B0() => if(carry) { do emit(B0()) } else { do emit(B1()) }; resume(()) + case B1() => if(carry) { do emit(B1()); carry = false } else { do emit(B0()) }; resume(()) + } +} +def groupBytesBE{ body: => Unit / emit[Bit] }: Unit / emit[Byte] = { + var next = 0 + var p = 128 + for[Bit]{body}{ b => + b match { + case B0() => () + case B1() => next = next + p + } + p = p / 2 + if(p == 0) { + do emit(next.toByte) + next = 0 + p = 128 + } + } +} +def groupBytesLE{ body: => Unit / emit[Bit] }: Unit / emit[Byte] = { + var next = 0 + var p = 1 + for[Bit]{body}{ b => + b match { + case B0() => () + case B1() => next = next + p + } + p = p * 2 + if(p == 256) { + do emit(next.toByte) + next = 0 + p = 1 + } + } +} +def groupBytes{ body: => Unit / emit[Bit] }: Unit / emit[Byte] = + groupBytesBE{body} + +def nth[A](n: Int){ body: => Unit / emit[A] }: A / Exception[MissingValue] = { + var m = n + try { + body() + val r: A = do raise[MissingValue](MissingValue(), "code in first did not emit any values") + r + } with emit[A] { a => + if (m == 0) { + a + } else { + m = m - 1 + resume(()) + } + } +} +def first[A]{ body: => Unit / emit[A] }: A / Exception[MissingValue] = { + try { + body() + val r: A = do raise[MissingValue](MissingValue(), "code in first did not emit any values") + r + } with emit[A] { a => a } +} +// Literals/splices +// ---------------- +effect BinSplices = { + splice[Unit], splice[Bit], + splice[Byte], + splice[LE[Int]], splice[BE[Int]], + splice[LE[Signed[Int]]], splice[BE[Signed[Int]]], + splice[OfWidth[LE[Int]]], splice[OfWidth[BE[Int]]], + splice[OfWidth[LE[Signed[Int]]]], splice[OfWidth[BE[Signed[Int]]]] +} +def bit{ body: => Unit / { literal, BinSplices } }: Unit / emit[Bit] = { + try { + ungroupBytes{ + try { + body() + } + with splice[LE[Int]] { i => bytesLE(i.raw); resume(()) } + with splice[BE[Int]] { i => bytesBE(i.raw); resume(()) } + with splice[LE[Signed[Int]]] { i => signedBytesLE(i.raw.raw); resume(()) } + with splice[BE[Signed[Int]]] { i => signedBytesBE(i.raw.raw); resume(()) } + } + } + with literal { s => + feed(s){ + exhaustively { + do read[Char]() match { + case '0' => do emit(B0()) + case '1' => do emit(B1()) + case _ => () + } + } + } + resume(()) + } + with splice[Unit] { _ => resume(()) } + with splice[Bit] { b => do emit(b); resume(()) } + with splice[Byte] { b => bits(b); resume(()) } + with splice[OfWidth[LE[Int]]] { i => + bitsLE(i.raw.raw, i.width) + resume(()) + } + with splice[OfWidth[LE[Signed[Int]]]] { i => + if(i.raw.raw.raw < 0){ + twoscomplementLE{ bitsLE(0 - i.raw.raw.raw, i.width) } + } else { + bitsLE(i.raw.raw.raw, i.width) + } + resume(()) + } + with splice[OfWidth[BE[Int]]] { i => + bitsBE(i.raw.raw, i.width) + resume(()) + } + with splice[OfWidth[BE[Signed[Int]]]] { i => + collectList[Bit]{ + if(i.raw.raw.raw < 0){ + twoscomplementLE{ bitsLE(0 - i.raw.raw.raw, i.width) } + } else { + bitsLE(i.raw.raw.raw, i.width) + } + }.reverse.each + resume(()) + } +} + +namespace examples { + def main() = { + mainSuite("Simple literals"){ + test("literal hex 10"){ assertEqual(x"10${()}", 16) } + test("literal hex ff"){ assertEqual(x"ff${()}", 255) } + test("literal char a"){ assertEqual(x"${'a'}", x"61${()}") } + test("literal string ba"){ assertEqual(x"${"ba"}", x"62${()}" * 256 + x"61${()}") } + test("int back-and-forth (17)"){ assertEqual(x"${17}", 17)} + test("int back-and-forth (17), explicit BE"){ assertEqual(x"${17.BE}", 17) } + test("int back-and-forth (17), explicit LE"){ assertEqual(x"${17.LE}", 17 * 256 * 256 * 256) } + test("byte 00101010"){ + with on[MissingValue].default{ assertEqual(true, false) } + assertEqual(first[Byte]{groupBytes{ bit"00101010${()}" }}.toInt, 42) + } + test("to bits and back"){ + with on[MissingValue].default{ assertEqual(true, false) } + [42.toByte, 12.toByte, 113.toByte, 0.toByte, 255.toByte].foreach{ v => + assertEqual(first[Byte]{ groupBytes{ bits(v) } }, v) + } + } + test("to bits and back LE bitorder"){ + with on[MissingValue].default{ assertEqual(true, false) } + [42.toByte, 12.toByte, 113.toByte, 0.toByte, 255.toByte].foreach{ v => + assertEqual(first[Byte]{ groupBytesLE{ bitsLE(v) } }, v) + } + } + test("to bits and back BE bitorder"){ + with on[MissingValue].default{ assertEqual(true, false) } + [42.toByte, 12.toByte, 113.toByte, 0.toByte, 255.toByte].foreach{ v => + assertEqual(first[Byte]{ groupBytesBE{ bitsBE(v) } }, v) + } + } + test("append 0 means *2"){ + with on[MissingValue].default{ assertEqual(true, false) } + [42.toByte, 12.toByte, 127.toByte].foreach{ v => + assertEqual(nth[Byte](1){ groupBytes{ repeat(7){ do emit(B0()) }; bits(v); do emit(B0()) } }, (v.toInt * 2).toByte) + } + } + test("pow agrees with double one"){ + assertEqual(pow(2,5), pow(2.0,5).toInt) + } + test("LE 2s-complement"){ + with on[MissingValue].default{ assertEqual(true, false) } + assertEqual(first[Byte]{ groupBytesLE{ twoscomplementLE{ bitsLE(6.toByte) } } }, 250.toByte) + } + test("BE 2s-complement"){ + with on[MissingValue].default{ assertEqual(true, false) } + assertEqual(first[Byte]{ groupBytesBE{ bit"${-6.Signed.BE.OfWidth(8)}" } }, 250.toByte) + } + } + } +} \ No newline at end of file diff --git a/libraries/common/binstream.effekt b/libraries/common/binstream.effekt new file mode 100644 index 000000000..413485766 --- /dev/null +++ b/libraries/common/binstream.effekt @@ -0,0 +1,401 @@ +import option +import stringbuffer +import stream +import char +import test +import io/error +import bytearray + +// assumes by default: +// BE byteorder, BE bitorder, unsigned + +// Wrappers +// -------- +record BE[A](raw: A) +record LE[A](raw: A) +record OfWidth[A](raw: A, width: Int) +record Signed[A](raw: A) + +/// Bits +type Bit { B0(); B1() } + +/// Effect alias +effect HexSplices = { + splice[Char], splice[String], + splice[Unit], + splice[Int], + splice[Byte], + splice[BE[Int]], splice[LE[Int]], + splice[LE[Signed[Int]]], splice[OfWidth[LE[Int]]], splice[OfWidth[LE[Signed[Int]]]], + splice[BE[Signed[Int]]], splice[OfWidth[BE[Int]]], splice[OfWidth[BE[Signed[Int]]]], + splice[ByteArray] +} + +// Splitting +// --------- +def bytesLE(int: Int, w: Int): Unit / emit[Byte] = { + var c = int + repeat(w){ + do emit(mod(c, 256).toByte) + c = c / 256 + } +} +def bytesLE(int: Int): Unit / emit[Byte] = bytesLE(int, 4) +def bytesBE(n: Int, width: Int): Unit / emit[Byte] = { + var pos = pow(256, width - 1) + repeat(width){ + do emit((bitwiseAnd(n, pos * 255) / pos).toByte) + pos = pos / 256 + } +} +def bytesBE(n: Int): Unit / emit[Byte] = bytesBE(n, 4) +def bytes(n: Int): Unit / emit[Byte] = bytesBE(n) +def signedBytesLE(int: Int, width: Int): Unit / emit[Byte] = { + if (int < 0) { + bytesLE(bitwiseNot(neg(int)) + 1, width) + } else { + bytesLE(int, width) + } +} +def signedBytesBE(int: Int, width: Int): Unit / emit[Byte] = { + if (int < 0) { + bytesBE(bitwiseNot(neg(int)) + 1, width) + } else { + bytesBE(int, width) + } +} +def signedBytesLE(int: Int): Unit / emit[Byte] = signedBytesLE(int, 4) +def signedBytesBE(int: Int): Unit / emit[Byte] = signedBytesBE(int, 4) +def bitsBE(int: Int): Unit / emit[Bit] = bitsBE(int, 32) +def collectBitsBE{ body: => Unit / emit[Bit] }: Int = { + var res = 0 + try body() with emit[Bit] { b => + res = b match { + case B0() => res * 2 + case B1() => res * 2 + 1 + } + resume(()) + } + res +} +def not(b: Bit): Bit = b match { + case B0() => B1() + case B1() => B0() +} +def bitwiseNot(n: Int): Int = { + collectBitsBE{ + try bitsBE(n) with emit[Bit]{ b => resume(do emit(not(b))) } + } +} + +// Splicers +// -------- + +def hex{ body: => Unit / { literal, HexSplices } }: Unit / emit[Byte] = { + try { + try { + body() + } + with splice[String] { s => + feed(s){ exhaustively{ do splice[Char](do read[Char]()) } } + resume(()) + } + with splice[ByteArray] { ba => + ba.foreach{ b => do splice[Byte](b) } + resume(()) + } + } + with literal { s => + feed(s){ + exhaustively { + with on[MissingValue].default{ () } + val upper: Int = hexDigitValue(do read[Char]()).value + val lower: Int = hexDigitValue(do read[Char]()).value + do emit[Byte]((16 * upper + lower).toByte) + } + } + resume(()) + } + with splice[Char] { c => do emit[Byte](c.toInt.toByte); resume(()) } + with splice[Byte] { b => do emit(b); resume(()) } + with splice[Unit] { u => resume(()) } + with splice[Int] { n => bytesBE(n); resume(()) } + with splice[LE[Int]] { w => bytesLE(w.raw); resume(()) } + with splice[BE[Int]] { v => bytesBE(v.raw); resume(()) } + with splice[LE[Signed[Int]]] { w => signedBytesLE(w.raw.raw); resume(()) } + with splice[OfWidth[LE[Int]]] { w => bytesLE(w.raw.raw, w.width); resume(()) } + with splice[OfWidth[LE[Signed[Int]]]] { w => signedBytesLE(w.raw.raw.raw, w.width); resume(()) } + with splice[BE[Signed[Int]]] { w => signedBytesBE(w.raw.raw); resume(()) } + with splice[OfWidth[BE[Int]]] { w => bytesBE(w.raw.raw, w.width); resume(()) } + with splice[OfWidth[BE[Signed[Int]]]] { w => signedBytesBE(w.raw.raw.raw, w.width); resume(()) } +} + +def x{ body: => Unit / { literal, HexSplices } }: Int = { + var res = 0 + for[Byte]{ hex{body} }{ v => res = res * 256 + v.toInt } + res +} + +// Counting and padding +// -------------------- +effect pad[A](fac: Int){ gen: => A }: Unit +effect getPos(): Int +def tracking[A]{ body: => Unit / { emit[A], getPos, pad[A] } }: Unit / emit[A] = { + var n = 0 + try body() + with emit[A] { b => n = n + 1; resume(do emit[A](b)) } + with getPos{ resume(n) } + with pad[A] { fac => + resume { {gen} => + while(mod(n, fac) != 0){ + do emit[A](gen()) + n = n + 1 + } + } + } +} + +// Sub-Byte +// ======== + +// From/to Bytes +// ------------- +def bitsLE(byte: Byte): Unit / emit[Bit] = { + val v = byte.toInt + var mask = 1 + repeat(8){ + bitwiseAnd(v, mask) match { + case 0 => do emit(B0()) + case _ => do emit(B1()) + } + mask = mask * 2 + } +} +def bitsBE(byte: Byte): Unit / emit[Bit] = { + val v = byte.toInt + var mask = 128 + repeat(8){ + bitwiseAnd(v, mask) match { + case 0 => do emit(B0()) + case _ => do emit(B1()) + } + mask = mask / 2 + } +} +def bits(byte: Byte): Unit / emit[Bit] = bitsBE(byte) +def bitsLE(v: Int, width: Int): Unit / emit[Bit] = { + var mask = 1 + repeat(width){ + bitwiseAnd(v, mask) match { + case 0 => do emit(B0()) + case _ => do emit(B1()) + } + mask = mask * 2 + } +} +def pow(n: Int, exp: Int): Int = { + def go(n: Int, exp: Int, acc: Int): Int = { + if (exp == 0) { + acc + } else if (mod(exp, 2) == 0) { + go(n * n, exp / 2, acc) + } else { + go(n * n, exp / 2, acc * n) + } + } + go(n, exp, 1) +} +def bitsBE(v: Int, width: Int): Unit / emit[Bit] = { + var mask = pow(2, width - 1) + repeat(width){ + bitwiseAnd(v, mask) match { + case 0 => do emit(B0()) + case _ => do emit(B1()) + } + mask = mask / 2 + } +} +def ungroupBytes{ body: => Unit / emit[Byte] }: Unit / emit[Bit] = + for[Byte]{body}{ b => bits(b) } +def twoscomplementLE{ body: => Unit / emit[Bit] }: Unit / emit[Bit] = { + var carry = true + try body() + with emit[Bit] { + case B0() => if(carry) { do emit(B0()) } else { do emit(B1()) }; resume(()) + case B1() => if(carry) { do emit(B1()); carry = false } else { do emit(B0()) }; resume(()) + } +} +def groupBytesBE{ body: => Unit / emit[Bit] }: Unit / emit[Byte] = { + var next = 0 + var p = 128 + for[Bit]{body}{ b => + b match { + case B0() => () + case B1() => next = next + p + } + p = p / 2 + if(p == 0) { + do emit(next.toByte) + next = 0 + p = 128 + } + } +} +def groupBytesLE{ body: => Unit / emit[Bit] }: Unit / emit[Byte] = { + var next = 0 + var p = 1 + for[Bit]{body}{ b => + b match { + case B0() => () + case B1() => next = next + p + } + p = p * 2 + if(p == 256) { + do emit(next.toByte) + next = 0 + p = 1 + } + } +} +def groupBytes{ body: => Unit / emit[Bit] }: Unit / emit[Byte] = + groupBytesBE{body} + +def nth[A](n: Int){ body: => Unit / emit[A] }: A / Exception[MissingValue] = { + var m = n + try { + body() + val r: A = do raise[MissingValue](MissingValue(), "code in first did not emit any values") + r + } with emit[A] { a => + if (m == 0) { + a + } else { + m = m - 1 + resume(()) + } + } +} +def first[A]{ body: => Unit / emit[A] }: A / Exception[MissingValue] = { + try { + body() + val r: A = do raise[MissingValue](MissingValue(), "code in first did not emit any values") + r + } with emit[A] { a => a } +} +// Literals/splices +// ---------------- +effect BinSplices = { + splice[Unit], splice[Bit], + splice[Byte], + splice[LE[Int]], splice[BE[Int]], + splice[LE[Signed[Int]]], splice[BE[Signed[Int]]], + splice[OfWidth[LE[Int]]], splice[OfWidth[BE[Int]]], + splice[OfWidth[LE[Signed[Int]]]], splice[OfWidth[BE[Signed[Int]]]] +} +def bit{ body: => Unit / { literal, BinSplices } }: Unit / emit[Bit] = { + try { + ungroupBytes{ + try { + body() + } + with splice[LE[Int]] { i => bytesLE(i.raw); resume(()) } + with splice[BE[Int]] { i => bytesBE(i.raw); resume(()) } + with splice[LE[Signed[Int]]] { i => signedBytesLE(i.raw.raw); resume(()) } + with splice[BE[Signed[Int]]] { i => signedBytesBE(i.raw.raw); resume(()) } + } + } + with literal { s => + feed(s){ + exhaustively { + do read[Char]() match { + case '0' => do emit(B0()) + case '1' => do emit(B1()) + case _ => () + } + } + } + resume(()) + } + with splice[Unit] { _ => resume(()) } + with splice[Bit] { b => do emit(b); resume(()) } + with splice[Byte] { b => bits(b); resume(()) } + with splice[OfWidth[LE[Int]]] { i => + bitsLE(i.raw.raw, i.width) + resume(()) + } + with splice[OfWidth[LE[Signed[Int]]]] { i => + if(i.raw.raw.raw < 0){ + twoscomplementLE{ bitsLE(0 - i.raw.raw.raw, i.width) } + } else { + bitsLE(i.raw.raw.raw, i.width) + } + resume(()) + } + with splice[OfWidth[BE[Int]]] { i => + bitsBE(i.raw.raw, i.width) + resume(()) + } + with splice[OfWidth[BE[Signed[Int]]]] { i => + collectList[Bit]{ + if(i.raw.raw.raw < 0){ + twoscomplementLE{ bitsLE(0 - i.raw.raw.raw, i.width) } + } else { + bitsLE(i.raw.raw.raw, i.width) + } + }.reverse.each + resume(()) + } +} + +namespace examples { + def main() = { + mainSuite("Simple literals"){ + test("literal hex 10"){ assertEqual(x"10${()}", 16) } + test("literal hex ff"){ assertEqual(x"ff${()}", 255) } + test("literal char a"){ assertEqual(x"${'a'}", x"61${()}") } + test("literal string ba"){ assertEqual(x"${"ba"}", x"62${()}" * 256 + x"61${()}") } + test("int back-and-forth (17)"){ assertEqual(x"${17}", 17)} + test("int back-and-forth (17), explicit BE"){ assertEqual(x"${17.BE}", 17) } + test("int back-and-forth (17), explicit LE"){ assertEqual(x"${17.LE}", 17 * 256 * 256 * 256) } + test("byte 00101010"){ + with on[MissingValue].default{ assertEqual(true, false) } + assertEqual(first[Byte]{groupBytes{ bit"00101010${()}" }}.toInt, 42) + } + test("to bits and back"){ + with on[MissingValue].default{ assertEqual(true, false) } + [42.toByte, 12.toByte, 113.toByte, 0.toByte, 255.toByte].foreach{ v => + assertEqual(first[Byte]{ groupBytes{ bits(v) } }, v) + } + } + test("to bits and back LE bitorder"){ + with on[MissingValue].default{ assertEqual(true, false) } + [42.toByte, 12.toByte, 113.toByte, 0.toByte, 255.toByte].foreach{ v => + assertEqual(first[Byte]{ groupBytesLE{ bitsLE(v) } }, v) + } + } + test("to bits and back BE bitorder"){ + with on[MissingValue].default{ assertEqual(true, false) } + [42.toByte, 12.toByte, 113.toByte, 0.toByte, 255.toByte].foreach{ v => + assertEqual(first[Byte]{ groupBytesBE{ bitsBE(v) } }, v) + } + } + test("append 0 means *2"){ + with on[MissingValue].default{ assertEqual(true, false) } + [42.toByte, 12.toByte, 127.toByte].foreach{ v => + assertEqual(nth[Byte](1){ groupBytes{ repeat(7){ do emit(B0()) }; bits(v); do emit(B0()) } }, (v.toInt * 2).toByte) + } + } + test("pow agrees with double one"){ + assertEqual(pow(2,5), pow(2.0,5).toInt) + } + test("LE 2s-complement"){ + with on[MissingValue].default{ assertEqual(true, false) } + assertEqual(first[Byte]{ groupBytesLE{ twoscomplementLE{ bitsLE(6.toByte) } } }, 250.toByte) + } + test("BE 2s-complement"){ + with on[MissingValue].default{ assertEqual(true, false) } + assertEqual(first[Byte]{ groupBytesBE{ bit"${-6.Signed.BE.OfWidth(8)}" } }, 250.toByte) + } + } + } +} \ No newline at end of file diff --git a/libraries/common/bitmap.effekt b/libraries/common/bitmap.effekt new file mode 100644 index 000000000..99ab715e5 --- /dev/null +++ b/libraries/common/bitmap.effekt @@ -0,0 +1,65 @@ +import binstream +import color +import array +import stream +import io/error + +record RGBBitmap(width: Int, height: Int, data: Array[RGB]) + +def bitmap(width: Int, height: Int): RGBBitmap = { + RGBBitmap(width, height, array(width * height, ColorNames::white)) +} +def bitmap(width: Int, height: Int, background: RGB): RGBBitmap = { + RGBBitmap(width, height, array(width * height, background)) +} +def setColor(bmp: RGBBitmap, x: Int, y: Int, color: RGB): Unit / Exception[OutOfBounds] = + bmp.data.set(x + y * bmp.width, color) +def getColor(bmp: RGBBitmap, x: Int, y: Int): RGB / Exception[OutOfBounds] = + bmp.data.get(x + y * bmp.width) + +def save(bmp: RGBBitmap, filename: String): Unit / Exception[IOError] = { + with on[OutOfBounds].panic + val headerSize = 54 + val padSize = mod(bmp.height, 4) + val columnSize = bmp.height * 3 + padSize + val totalSize = headerSize + columnSize * bmp.width + val pixelDataStart = headerSize + writeFile(filename){ + // File header + hex"${"BM"} ${totalSize.LE} ${0.LE} ${pixelDataStart.LE}" + // DIB header (BITMAPINFOHEADER) + hex"${40.LE}" + hex"${bmp.width.Signed.LE}${bmp.height.Signed.LE}" // w x h + hex"${1.LE.OfWidth(2)}" // 1 color plane + hex"${24.LE.OfWidth(2)}" // bpp + hex"${0.LE}" // no compression (BI_RGB) + hex"${(columnSize * bmp.width).LE}" // raw size of bitmap data + hex"${2835.Signed.LE}${2835.Signed.LE}" // resolution (px/m) + hex"${0.LE}${0.LE}" + // Image data + tracking[Byte]{ + each(0, bmp.width){ x => + each(0, bmp.height){ y => + val c = bmp.getColor(x, y) + hex"${toInt(c.b * 255.0).toByte}${toInt(c.g * 255.0).toByte}${toInt(c.r * 255.0).toByte}" + } + do pad[Byte](4){ 0.toByte } + } + } + } +} + +namespace examples { + def main() = { + with on[OutOfBounds].panic + with on[IOError].panic + val size = 600 + val bmp = bitmap(size, size, ColorNames::white) + each(0, size){ x => + each(0, size){ y => + bmp.setColor(x, y, HSV(x.toDouble / size.toDouble, 1.0, y.toDouble / size.toDouble).asRGB) + } + } + bmp.save("./test.bmp") + } +} \ No newline at end of file diff --git a/libraries/common/canvas.effekt b/libraries/common/canvas.effekt new file mode 100644 index 000000000..55f6f7d83 --- /dev/null +++ b/libraries/common/canvas.effekt @@ -0,0 +1,210 @@ +import path +import vec +import color +import draw +import stringbuffer +import stream + +// bindings to use the draw library using a HTML Canvas + +// # Internal bindings, implementation + +extern type DrawingContext2D +namespace internal { + extern io def getDrawingContext(canvasId: String): DrawingContext2D = + jsWeb "document.getElementById(${canvasId}).getContext(\"2d\")" + extern io def beginPath(ctx: DrawingContext2D): Unit = + jsWeb "${ctx}.beginPath()" + extern io def moveTo(ctx: DrawingContext2D, x: Double, y: Double): Unit = + jsWeb "${ctx}.moveTo(${x}, ${y})" + extern io def lineTo(ctx: DrawingContext2D, x: Double, y: Double): Unit = + jsWeb "${ctx}.lineTo(${x}, ${y})" + extern io def stroke(ctx: DrawingContext2D): Unit = + jsWeb "${ctx}.stroke()" + extern io def fill(ctx: DrawingContext2D): Unit = + jsWeb "${ctx}.fill()" + extern io def clearRect(ctx: DrawingContext2D, x: Double, y: Double, width: Double, height: Double): Unit = + jsWeb "${ctx}.clearRect(${x}, ${y}, ${width}, ${height})" + extern io def fillRect(ctx: DrawingContext2D, x: Double, y: Double, width: Double, height: Double): Unit = + jsWeb "${ctx}.fillRect(${x}, ${y}, ${width}, ${height})" + extern io def strokeRect(ctx: DrawingContext2D, x: Double, y: Double, width: Double, height: Double): Unit = + jsWeb "${ctx}.strokeRect(${x}, ${y}, ${width}, ${height})" + extern io def fillText(ctx: DrawingContext2D, text: String, x: Double, y: Double): Unit = + jsWeb "${ctx}.fillText(${text}, ${x}, ${y})" + extern io def closePath(ctx: DrawingContext2D): Unit = + jsWeb "${ctx}.closePath()" + extern io def bezierCurveTo(ctx: DrawingContext2D, cp1x: Double, cp1y: Double, cp2x: Double, cp2y: Double, x: Double, y: Double): Unit = + jsWeb "${ctx}.bezierCurveTo(${cp1x}, ${cp1y}, ${cp2x}, ${cp2y}, ${x}, ${y})" + extern io def quadraticCurveTo(ctx: DrawingContext2D, cpx: Double, cpy: Double, x: Double, y: Double): Unit = + jsWeb "${ctx}.quadraticCurveTo(${cpx}, ${cpy}, ${x}, ${y})" + extern io def ellipse(ctx: DrawingContext2D, x: Double, y: Double, radiusX: Double, radiusY: Double, rotation: Double, startAngle: Double, endAngle: Double, anticlockwise: Bool): Unit = + jsWeb "${ctx}.ellipse(${x}, ${y}, ${radiusX}, ${radiusY}, ${rotation}, ${startAngle}, ${endAngle}, ${anticlockwise})" + extern io def setFillStyle(ctx: DrawingContext2D, color: String): Unit = + jsWeb "${ctx}.fillStyle = ${color}" + extern io def setFont(ctx: DrawingContext2D, size: Double, family: String): Unit = + jsWeb "${ctx}.font = ${size} + 'px ' + ${family}" + extern io def setStrokeStyle(ctx: DrawingContext2D, color: String): Unit = + jsWeb "${ctx}.strokeStyle = ${color}" + extern io def setLineWidth(ctx: DrawingContext2D, width: Double): Unit = + jsWeb "${ctx}.lineWidth = ${width}" + + def svgArcTo(ctx: DrawingContext2D, x1: Double, y1: Double, rx: Double, ry: Double, xAxisRotation: Double, largeArcFlag: Bool, sweepFlag: Bool, x2: Double, y2: Double): Unit = { + var rx = rx + var ry = ry + // Convert rotation from degrees to radians + val angleRad = xAxisRotation * PI / 180.0; + + // Step 2: Transform to origin + val dx = (x1 - x2) / 2.0; + val dy = (y1 - y2) / 2.0; + + // Transform point into coordinate space of the ellipse + val x1Prime = dx * cos(angleRad) + dy * sin(angleRad); + val y1Prime = (1.0 - dx) * sin(angleRad) + dy * cos(angleRad); + + // Step 3: Ensure radii are large enough + val lambda = (x1Prime * x1Prime) / (rx * rx) + (y1Prime * y1Prime) / (ry * ry); + if (lambda > 1.0) { + rx = rx * sqrt(lambda); + ry = ry * sqrt(lambda); + } + + // Step 4: Compute center parameters + val sign = if (largeArcFlag == sweepFlag) {-1.0} else {1.0}; + val sq = ((rx * rx * ry * ry) - (rx * rx * y1Prime * y1Prime) - (ry * ry * x1Prime * x1Prime)) / + ((rx * rx * y1Prime * y1Prime) + (ry * ry * x1Prime * x1Prime)); + val coef = sign * sqrt(max(0.0, sq)); + + val cxPrime = coef * ((rx * y1Prime) / ry); + val cyPrime = coef * (0.0 - (ry * x1Prime) / rx); + + // Step 5: Transform back to original coordinate space + val cx = cos(angleRad) * cxPrime - sin(angleRad) * cyPrime + (x1 + x2) / 2.0; + val cy = sin(angleRad) * cxPrime + cos(angleRad) * cyPrime + (y1 + y2) / 2.0; + + // Step 6: Compute start and sweep angles + val startAngle = atan2( + (y1Prime - cyPrime) / ry, + (x1Prime - cxPrime) / rx + ); + + var deltaAngle = atan2( + (0.0 - y1Prime - cyPrime) / ry, + (0.0 - x1Prime - cxPrime) / rx + ) - startAngle; + + // Adjust sweep angle according to flags + if (not(sweepFlag) && deltaAngle > 0.0) { + deltaAngle = deltaAngle - 2.0 * PI; + } else if (sweepFlag && deltaAngle < 0.0) { + deltaAngle = deltaAngle + 2.0 * PI; + } + + ctx.internal::ellipse(cx, cy, rx, ry, xAxisRotation, startAngle, startAngle + deltaAngle, sweepFlag) +} + + def asCanvasPath(ctx: DrawingContext2D){ body: => Unit / Path2D }: Unit = { + var current = Vec2D(0.0, 0.0) + try body() with Path2D { + def moveTo(p) = { + current = p + resume(ctx.internal::moveTo(p.x, p.y)) + } + def lineTo(p) = { + current = p + resume(ctx.internal::lineTo(p.x, p.y)) + } + def cubicBezierTo(c1, c2, t) = { + current = t + resume(ctx.internal::bezierCurveTo(c1.x, c1.y, c2.x, c2.y, t.x, t.y)) + } + def quadraticBezierTo(c, t) = { + current = t + resume(ctx.internal::quadraticCurveTo(c.x, c.y, t.x, t.y)) + } + def arc(rx, ry, rot, largeArc, sweep, to) = { + ctx.internal::svgArcTo(current.x, current.y, rx, ry, rot, largeArc, sweep, to.x, to.y) + current = to + resume(()) + } + } + } + + extern io def mkCanvas(id: String, w: Int, h: Int): Unit = + jsWeb """document.body.innerHTML += "" """ + extern io def addToBody(str: String): Unit = + jsWeb """document.body.innerHTML += ${str} """ +} + +/// Draw into the given 2d drawing context +def onDrawingContext(ctx: DrawingContext2D){ body: => Unit / Draw }: Unit = { + try body() with Draw { + def stroke() = resume { {body} => + ctx.internal::setStrokeStyle(do strokeColor().toHTML) + ctx.internal::asCanvasPath{ body() } + ctx.internal::stroke() + } + def fill() = resume { {body} => + ctx.internal::setFillStyle(do fillColor().toHTML) + ctx.internal::asCanvasPath{ body() } + ctx.internal::fill() + } + def text(pos, text) = resume { + ctx.internal::setFillStyle(do fontColor().toHTML) + ctx.internal::setFont(do fontSize(), "serif") + ctx.internal::fillText(text, pos.x, pos.y) + } + } +} +/// Draw onto the canvas with the given id +def onCanvas(id: String){ body: => Unit / Draw }: Unit = { + onDrawingContext(internal::getDrawingContext(id)){body} +} + +namespace example { + def main() = { + with on[MissingValue].panic + def snowflake(from: Vec2D, to: Vec2D, d: Int): Unit / Path2D = { + if (d == 0) { + do moveTo(from); do lineTo(to) + } else { + val third = (to - from) / 3.0 + val p1 = from + third + val p2 = from + 2.0 * third + val peak = p1 + third.rotate(PI / -3.0) + + snowflake(from, p1, d - 1) + snowflake(p1, peak, d - 1) + snowflake(peak, p2, d - 1) + snowflake(p2, to, d - 1) + } + } + def img(): Unit / Path2D = scale(3.5){ translate(Vec2D(0.0, -50.0)){ rotate(Vec2D(0.0, 100.0), 0.2 * PI) { simplify { + snowflake(Vec2D(0.0, 100.0), Vec2D(100.0, 100.0), 5) + rect(Vec2D(0.0, 0.0), Vec2D(10.0, 10.0)) + circle(Vec2D(5.0, 5.0), 5.0) + }}}} + + pathBoundingBox { img() } match { case (tl, br) => + try { + stringBuffer { + try { + asSVG{ + do stroke{ img() } + do text(Vec2D(0.0, 20.0), "Hallo") + } + } with emit[String]{ s => resume(do write(s)) } + internal::addToBody(do flush()) + } + + internal::mkCanvas("myCanvas", (br - tl).x.toInt + 1, (br - tl).y.toInt + 1) + onCanvas("myCanvas"){ + do stroke { translate(-1.0 * tl){img()} } + do text(Vec2D(0.0,20.0) - tl,"Hallo") + } + } with strokeColor { () => resume(ColorNames::red.withAlpha(1.0)) } + with fontColor { () => resume(ColorNames::blue.withAlpha(1.0)) } + with fontSize { () => resume(20.0) } + } + } +} \ No newline at end of file diff --git a/libraries/common/color.effekt b/libraries/common/color.effekt new file mode 100644 index 000000000..7f019d117 --- /dev/null +++ b/libraries/common/color.effekt @@ -0,0 +1,369 @@ +import stringbuffer +import io/console +import stream + +// Basic color library + +// # Helpers + +def max(a: Double, b: Double, c: Double): Double = { + if (a >= b && a >= c) a + else if (b >= a && b >= c) b + else c +} +def min(a: Double, b: Double, c: Double): Double = { + if (a <= b && a <= c) a + else if (b <= a && b <= c) b + else c +} +def mod(a: Double, b: Double): Double = { + a - b * (a / b).floor.toDouble +} +def force01(v: Double): Double = { + if (v < 0.0) { 0.0 } else if (v > 1.0) { 1.0 } else v +} + +// # Record types for different color spaces + +/// red, greeen, blue +record RGB(r: Double, g: Double, b: Double) +/// hue, saturation, value +record HSV(h: Double, s: Double, v: Double) +/// cyan, magenta, yellow, key +record CMYK(c: Double, m: Double, y: Double, k: Double) +/// simulating human perception +record LMS(l: Double, m: Double, s: Double) +/// RGB plus alpha +record RGBA(r: Double, g: Double, b: Double, a: Double) + +// # Standard colors by name + +namespace ColorNames { + val black = RGB(0.0, 0.0, 0.0) + val white = RGB(1.0, 1.0, 1.0) + val red = RGB(1.0, 0.0, 0.0) + val lime = RGB(0.0, 1.0, 0.0) + val blue = RGB(0.0, 0.0, 1.0) + val yellow = RGB(1.0, 1.0, 0.0) + val cyan = RGB(0.0, 1.0, 1.0) + val magenta = RGB(1.0, 0.0, 1.0) + val silver = RGB(0.75, 0.75, 0.75) + val gray50 = RGB(0.5, 0.5, 0.5) + def gray(factor: Double) = RGB(factor, factor, factor) + val maroon = RGB(0.5, 0.0, 0.0) + val olive = RGB(0.5, 0.5, 0.0) + val green = RGB(0.0, 0.5, 0.0) + val purple = RGB(0.5, 0.0, 0.5) + val teal = RGB(0.0, 0.5, 0.5) + val navy = RGB(0.0, 0.0, 0.5) +} + +// # Conversions between color spaces + +def asCMYK(rgb: RGB): CMYK = { + val k = 1.0 - max(rgb.r, rgb.g, rgb.b) + val c = (1.0 - rgb.r - k) / (1.0 - k) + val m = (1.0 - rgb.g - k) / (1.0 - k) + val y = (1.0 - rgb.b - k) / (1.0 - k) + CMYK(c, m, y, k) +} +def asRGB(cmyk: CMYK): RGB = { + val r = (1.0 - cmyk.c) * (1.0 - cmyk.k) + val g = (1.0 - cmyk.m) * (1.0 - cmyk.k) + val b = (1.0 - cmyk.y) * (1.0 - cmyk.k) + RGB(r, g, b) +} +def withAlpha(rgb: RGB, alpha: Double) = RGBA(rgb.r, rgb.g, rgb.b, alpha) + +def asHSV(rgb: RGB): HSV = { + val maxVal = max(rgb.r, rgb.g, rgb.b) + val minVal = min(rgb.r, rgb.g, rgb.b) + val delta = maxVal - minVal + + val h = if (delta == 0.0) 0.0 + else if (maxVal == rgb.r) mod((rgb.g - rgb.b) / delta, 6.0) + else if (maxVal == rgb.g) ((rgb.b - rgb.r) / delta) + 2.0 + else ((rgb.r - rgb.g) / delta) + 4.0 + + val hue = mod(h / 6.0 + 1.0, 1.0) + val saturation = if (maxVal == 0.0) 0.0 else delta / maxVal + val value = maxVal + + HSV(hue, saturation, value) +} + +def asRGB(hsv: HSV): RGB = { + val c = hsv.v * hsv.s + val x = c * (1.0 - abs(mod(hsv.h * 6.0, 2.0) - 1.0)) + val m = hsv.v - c + + val (r, g, b) = if (hsv.h < 1.0 / 6.0) (c, x, 0.0) + else if (hsv.h < 2.0 / 6.0) (x, c, 0.0) + else if (hsv.h < 3.0 / 6.0) (0.0, c, x) + else if (hsv.h < 4.0 / 6.0) (0.0, x, c) + else if (hsv.h < 5.0 / 6.0) (x, 0.0, c) + else (c, 0.0, x) + + RGB(r + m, g + m, b + m) +} + +def asLMS(rgb: RGB): LMS = { + // see https://ixora.io/projects/colorblindness/color-blindness-simulation-research/ + val l = 0.31399022 * rgb.r + 0.63951294 * rgb.g + 0.04649755 * rgb.b + val m = 0.15537241 * rgb.r + 0.75789446 * rgb.g + 0.08670142 * rgb.b + val s = 0.01775239 * rgb.r + 0.10944209 * rgb.g + 0.87256922 * rgb.b + LMS(l, m, s) +} + +def asRGB(lms: LMS): RGB = { + // see https://ixora.io/projects/colorblindness/color-blindness-simulation-research/ + val r = force01(5.47221206 * lms.l - 4.6419601 * lms.m + 0.16963708 * lms.s) + val g = force01(-1.1252419 * lms.l + 2.29317094 * lms.m - 0.1678952 * lms.s) + val b = force01(0.02980165 * lms.l - 0.19318073 * lms.m + 1.16364789 * lms.s) + RGB(r, g, b) +} + +// # Color blindness simulation +// see https://ixora.io/projects/colorblindness/color-blindness-simulation-research/ + +def asProtanopia(rgb: RGB): RGB = { + val lms = asLMS(rgb) + val l = force01(1.05118294 * lms.m - 0.05116099 * lms.s) + val m = force01(lms.m) + val s = force01(lms.s) + LMS(l, m, s).asRGB +} + +def asDeuteranopia(rgb: RGB): RGB = { + val lms = asLMS(rgb) + val l = force01(lms.l) + val m = force01(0.9513092 * lms.l + 0.04866992 * lms.s) + val s = force01(lms.s) + LMS(l, m, s).asRGB +} + +def asTritanopia(rgb: RGB): RGB = { + val lms = asLMS(rgb) + val l = force01(lms.l) + val m = force01(1.0 * lms.m) + val s = force01(-0.86744736 * lms.l + 1.86727089 * lms.m) + LMS(l, m, s).asRGB +} + +// # Relative luminance and contrast + +def relativeLuminance(rgb: RGB): Double = { + def adjust(c: Double): Double = { + if (c <= 0.03928) c / 12.92 + else ((c + 0.055) / 1.055).pow(2.4) + } + 0.2126 * adjust(rgb.r) + 0.7152 * adjust(rgb.g) + 0.0722 * adjust(rgb.b) +} +def contrastRatio(rgb1: RGB, rgb2: RGB): Double = { + val lum1 = relativeLuminance(rgb1) + val lum2 = relativeLuminance(rgb2) + if (lum1 > lum2) (lum1 + 0.05) / (lum2 + 0.05) + else (lum2 + 0.05) / (lum1 + 0.05) +} + +// # Color transformations + +def complement(rgb: RGB): RGB = { + RGB(1.0 - rgb.r, 1.0 - rgb.g, 1.0 - rgb.b) +} +def mix(rgb1: RGB, rgb2: RGB, ratio: Double): RGB = { + val r = rgb1.r * ratio + rgb2.r * (1.0 - ratio) + val g = rgb1.g * ratio + rgb2.g * (1.0 - ratio) + val b = rgb1.b * ratio + rgb2.b * (1.0 - ratio) + RGB(r, g, b) +} +def mix(rgb1: RGB, rgb2: RGB): RGB = { + RGB((rgb1.r + rgb2.r) / 2.0, (rgb1.g + rgb2.g) / 2.0, (rgb1.b + rgb2.b) / 2.0) +} +def shade(rgb: RGB, by: Double): RGB = mix(rgb, ColorNames::black, by) +def tint(rgb: RGB, by: Double): RGB = mix(rgb, ColorNames::white, by) + +def tone(rgb: RGB, by: Double): RGB = { + val amnt = (rgb.r + rgb.g + rgb.b) / 3.0 + mix(rgb, ColorNames::gray(amnt), by) +} +def rotate(rgb: RGB, angle: Double): RGB = { + val hsv = asHSV(rgb) + val newHue = mod(hsv.h + angle / 360.0, 1.0) + asRGB(HSV(newHue, hsv.s, hsv.v)) +} + +// # Generating simple standard palettes + +def splitComplementary(rgb: RGB, angle: Double): (RGB, RGB) = { + val c = rgb.complement + (rotate(c, angle), rotate(c, 0.0 - angle)) +} +def splitComplementary(rgb: RGB): (RGB, RGB) = splitComplementary(rgb, 30.0) +def analogous(rgb: RGB, angle: Double): (RGB, RGB) = { + (rotate(rgb, angle), rotate(rgb, 0.0 - angle)) +} +def analogous(rgb: RGB): (RGB, RGB) = analogous(rgb, 30.0) +def triadic(rgb: RGB): (RGB, RGB) = { + (rotate(rgb, 120.0), rotate(rgb, 240.0)) +} +def square(rgb: RGB): (RGB, RGB, RGB) = { + (rotate(rgb, 90.0), rotate(rgb, 180.0), rotate(rgb, 270.0)) +} +def tetradic(rgb: RGB, angle: Double): (RGB, RGB, RGB) = { + (rotate(rgb, angle), rotate(rgb, 180.0), rotate(rgb, 180.0 + angle)) +} +def tetradic(rgb: RGB): (RGB, RGB, RGB) = tetradic(rgb, 30.0) + +def binsplit(rgb: RGB): Unit / emit[RGB] = { + var a = 0 + var b = 2 + while (true) { + do emit(rgb.rotate(360.0 * (2 * a + 1).toDouble / b.toDouble)) + a = a + 1 + if (a * 2 + 1 > b) { + b = b * 2 + a = 0 + } + } +} +def evenlySpaced(rgb: RGB, n: Int): Unit / emit[RGB] = { + each(1, n){ i => + do emit(rgb.rotate(i.toDouble * 360.0 / n.toDouble)) + } +} + +// # Conversion to output formats + +def toAnsiForeground(rgb: RGB): String = { + val r = (rgb.r * 255.0).toInt + val g = (rgb.g * 255.0).toInt + val b = (rgb.b * 255.0).toInt + "\u001b[38;2;${r.show};${g.show};${b.show}m" +} + +def toAnsi256Foreground(rgb: RGB): String = { + val r = (rgb.r * 5.0).toInt + val g = (rgb.g * 5.0).toInt + val b = (rgb.b * 5.0).toInt + val index = 16 + 36 * r + 6 * g + b; + "\u001b[38;5;${index.show}m" +} + +def toAnsiBackground(rgb: RGB): String = { + val r = (rgb.r * 255.0).toInt + val g = (rgb.g * 255.0).toInt + val b = (rgb.b * 255.0).toInt + "\u001b[48;2;${r.show};${g.show};${b.show}m" +} +def toAnsi256Background(rgb: RGB): String = { + val r = (rgb.r * 5.0).toInt + val g = (rgb.g * 5.0).toInt + val b = (rgb.b * 5.0).toInt + val index = 16 + 36 * r + 6 * g + b; + "\u001b[48;5;${index.show}m" +} +def toHTML(rgb: RGB): String = { + val r = (rgb.r * 255.0).toInt + val g = (rgb.g * 255.0).toInt + val b = (rgb.b * 255.0).toInt + s"rgb(${r.show}, ${g.show}, ${b.show})" +} +def toHTML(rgb: RGBA): String = { + val r = (rgb.r * 255.0).toInt + val g = (rgb.g * 255.0).toInt + val b = (rgb.b * 255.0).toInt + val a = (rgb.a * 255.0).toInt + s"rgba(${r.show}, ${g.show}, ${b.show}, ${a.show})" +} +def toTikZ(rgb: RGB): String = { + val r = (rgb.r * 255.0).toInt + val g = (rgb.g * 255.0).toInt + val b = (rgb.b * 255.0).toInt + s"{rgb,255:red,${r};green,${g};blue,${b}}" +} + +// # Example + +namespace example { + def main() = console { + with on[WrongFormat].panic() + stringBuffer { + each(0, 129) { h => + val rgb = asRGB(HSV((h.toDouble * 2.0) / 255.0, 1.0, 1.0)) + val ansi = toAnsiForeground(rgb) + do write(s"${ansi}█") + } + println(do flush()) + } + stringBuffer { + each(0, 129) { h => + val rgb = asRGB(HSV((h.toDouble * 2.0 + 1.0) / 255.0, 1.0, 1.0)) + val ansi = toAnsiForeground(rgb) + do write(s"${ansi}█") + } + println(do flush()) + } + stringBuffer { + each(0, 128) { h => + val rgb = asRGB(HSV((h.toDouble * 2.0) / 255.0, 1.0, 1.0)) + val ansi = toAnsiForeground(rgb.asProtanopia) //.tint(0.5)) + do write(s"${ansi}█") + } + println(do flush()) + } + stringBuffer { + each(0, 128) { h => + val rgb = asRGB(HSV((h.toDouble * 2.0) / 255.0, 1.0, 1.0)) + val ansi = toAnsiForeground(rgb.asDeuteranopia) //.tint(0.5)) + do write(s"${ansi}█") + } + println(do flush()) + } + stringBuffer { + each(0, 128) { h => + val rgb = asRGB(HSV((h.toDouble * 2.0) / 255.0, 1.0, 1.0)) + val ansi = toAnsiForeground(rgb.asTritanopia) //.tint(0.5)) + do write(s"${ansi}█") + } + println(do flush()) + } + + val b = HSV(random(), 1.0, 1.0).asRGB + println(" ${b.toAnsiForeground}█${ColorNames::black.toAnsiForeground}") + println("shade: ${b.shade(0.5).toAnsiForeground}█${ColorNames::black.toAnsiForeground}") + println("tone: ${b.tone(0.5).toAnsiForeground}█${ColorNames::black.toAnsiForeground}") + println("tint: ${b.tint(0.5).toAnsiForeground}█${ColorNames::black.toAnsiForeground}") + println("compl: ${b.complement.toAnsiForeground}█${ColorNames::black.toAnsiForeground}") + val (l, r) = b.splitComplementary + println("split: ${l.toAnsiForeground}█${b.toAnsiForeground}█${r.toAnsiForeground}█${ColorNames::black.toAnsiForeground}") + val (l, r) = b.analogous + println("analogous: ${l.toAnsiForeground}█${b.toAnsiForeground}█${r.toAnsiForeground}█${ColorNames::black.toAnsiForeground}") + val (l, r) = b.triadic + println("triadic: ${l.toAnsiForeground}█${b.toAnsiForeground}█${r.toAnsiForeground}█${ColorNames::black.toAnsiForeground}") + val (l, m, r) = b.square + println("square: ${l.toAnsiForeground}█${b.toAnsiForeground}█${m.toAnsiForeground}█${r.toAnsiForeground}█${ColorNames::black.toAnsiForeground}") + val (l, m, r) = b.tetradic + println("tetradic: ${l.toAnsiForeground}█${b.toAnsiForeground}█${m.toAnsiForeground}█${r.toAnsiForeground}█${ColorNames::black.toAnsiForeground}") + stringBuffer { + boundary{ + source[RGB]{ do emit(b); binsplit(b) }{ + each(0, 10){ i => + do write("${do read[RGB]().toAnsiForeground}█${ColorNames::black.toAnsiForeground}") + } + } + } + println("binsplit: ${do flush()}") + } + stringBuffer { + boundary{ + source[RGB]{ do emit(b); evenlySpaced(b, 10) }{ + each(0, 10){ i => + do write("${do read[RGB]().toAnsiForeground}█${ColorNames::black.toAnsiForeground}") + } + } + } + println("evenly: ${do flush()}") + } + } +} \ No newline at end of file diff --git a/libraries/common/draw.effekt b/libraries/common/draw.effekt new file mode 100644 index 000000000..c81584793 --- /dev/null +++ b/libraries/common/draw.effekt @@ -0,0 +1,71 @@ +import stream +import vec +import color +import path +import stringbuffer + +// simple drawing library + +effect fillColor(): RGBA +effect FillStyle = { fillColor } + +effect strokeColor(): RGBA +effect StrokeStyle = { strokeColor } + +effect fontColor(): RGBA +effect fontSize(): Double +effect FontStyle = { fontColor, fontSize } + +effect Style = { FillStyle, StrokeStyle, FontStyle } + +interface Draw { + def fill(){ body: => Unit / Path2D }: Unit / FillStyle + def stroke(){ body: => Unit / Path2D }: Unit / StrokeStyle + def text(pos: Vec2D, text: String): Unit / FontStyle +} + +/// Compute a bounding box for the given drawing as (top-left, bottom-right) +def boundingBox{ body: => Unit / Draw }: (Vec2D, Vec2D) / Exception[MissingValue] = { + pathBoundingBox{ + try body() with Draw { + def fill() = resume { {body} => body() } + def stroke() = resume { {body} => body() } + def text(pos, text) = resume { do moveTo(pos); do moveTo(pos - Vec2D(0.0, do fontSize())) } // TODO + } + } +} + +/// emit drawing as a SVG (token-by-token) +def asSVG(w: Int, h: Int){ body: => Unit / {Draw} }: Unit / { emit[String] } = { + do emit("\n") + try body() with Draw { + def fill() = resume { {body} => + val path = asSvgPath { body() } + do emit("") + } + def stroke() = resume { {body} => + val path = asSvgPath { body() } + do emit("") + } + def text(pos, text) = resume { + do emit("") + do emit(text) + do emit("") + } + } + do emit("") +} + +/// emit as a SVG, scaling to content (re-runs body!) +def asSVG{ body: => Unit / Draw }: Unit / {emit[String], Exception[MissingValue]} = { + boundingBox{body} match { + case (tl, br) => + asSVG((br - tl).x.toInt + 1, (br - tl).y.toInt + 1) { + try body() with Draw { + def fill() = resume { {body} => do fill{ translate(-1.0 * tl){body} } } + def stroke() = resume { {body} => do stroke{ translate(-1.0 * tl){body} } } + def text(pos, text) = resume { do text(pos - tl, text) } + } + } + } +} \ No newline at end of file diff --git a/libraries/common/path.effekt b/libraries/common/path.effekt new file mode 100644 index 000000000..3de383697 --- /dev/null +++ b/libraries/common/path.effekt @@ -0,0 +1,310 @@ +import vec +import stringbuffer +import stream + +// Library for simple SVG-like paths + +// # Basic paths + +interface Path2D { + def moveTo(pos: Vec2D): Unit + def lineTo(pos: Vec2D): Unit + def cubicBezierTo(control1: Vec2D, control2: Vec2D, to: Vec2D): Unit + def quadraticBezierTo(control: Vec2D, to: Vec2D): Unit + def arc(rx: Double, ry: Double, rot: Double, largeArc: Bool, sweep: Bool, to: Vec2D): Unit +} + +/// Compute a bounding box for the given path in the format top-left, bottom-right. +/// Raises MissingValue if no drawing occured. +def pathBoundingBox{ body: => Unit / Path2D }: (Vec2D, Vec2D) / Exception[MissingValue] = { + var top: Option[Double] = None() + var left: Option[Double] = None() + var bot: Option[Double] = None() + var right: Option[Double] = None() + + def seeX(x: Double): Unit = { + if (left is None()) { left = Some(x) } + else if (left is Some(l) and x < l) { + left = Some(x) + } + if (right is None()) { right = Some(x) } + else if (right is Some(r) and x > r) { + right = Some(x) + } + } + def seeY(y: Double): Unit = { + if (top is None()) { top = Some(y) } + else if (top is Some(t) and y < t) { + top = Some(y) + } + if (bot is None()) { bot = Some(y) } + else if (bot is Some(b) and y > b) { + bot = Some(y) + } + } + + try body() with Path2D { + def moveTo(pos) = { + seeX(pos.x); seeY(pos.y); resume(()) + } + def lineTo(pos) = { + seeX(pos.x); seeY(pos.y); resume(()) + } + def cubicBezierTo(control1, control2, to) = { + seeX(to.x); seeY(to.y) // TODO compute actual extent + resume(()) + } + def quadraticBezierTo(control, to) = { + seeX(to.x); seeY(to.y) // TODO compute actual extent + resume(()) + } + def arc(rx, ry, rot, largeArc, sweep, to) = { + seeX(to.x); seeY(to.y) // TODO compute actual extent + resume(()) + } + } + (Vec2D(left.value, top.value), Vec2D(right.value, bot.value)) +} + +/// Simplify the path by removing moveTo and lineTo to the current position +def simplify(){ body: => Unit / Path2D }: Unit / Path2D = { + var pos: Option[Vec2D] = None() + try body() with Path2D { + def moveTo(p) = { + if (pos is Some(c) and p == c) { pos = Some(p); resume(()) } else { pos = Some(p); do moveTo(p); resume(()) } + } + def lineTo(p) = { + if (pos is Some(c) and p == c) { pos = Some(p); resume(()) } else { pos = Some(p); do lineTo(p); resume(()) } + } + def cubicBezierTo(c1, c2, t) = { + pos = Some(t) + do cubicBezierTo(c1, c2, t) + resume(()) + } + def quadraticBezierTo(c, t) = { + pos = Some(t) + do quadraticBezierTo(c, t) + resume(()) + } + def arc(rx, ry, rot, largeArc, sweep, to) = { + pos = Some(to) + do arc(rx, ry, rot, largeArc, sweep, to) + resume(()) + } + } +} + +// # Standard shapes + +def rect(topleft: Vec2D, bottomright: Vec2D): Unit / Path2D = { + do moveTo(topleft) + do lineTo(Vec2D(bottomright.x, topleft.y)) + do lineTo(bottomright) + do lineTo(Vec2D(topleft.x, bottomright.y)) + do lineTo(topleft) +} +def circle(center: Vec2D, radius: Double): Unit / Path2D = { + val start = Vec2D(center.x + radius, center.y) + do moveTo(start) + do arc(radius, radius, 0.0, false, true, Vec2D(center.x - radius, center.y)) + do arc(radius, radius, 0.0, false, true, start) +} +def ellipse(center: Vec2D, rx: Double, ry: Double): Unit / Path2D = { + val start = Vec2D(center.x + rx, center.y) + do moveTo(start) + do arc(rx, ry, 0.0, false, true, Vec2D(center.x - rx, center.y)) + do arc(rx, ry, 0.0, false, true, start) +} + +def triangle(p1: Vec2D, p2: Vec2D, p3: Vec2D): Unit / Path2D = { + do moveTo(p1) + do lineTo(p2) + do lineTo(p3) + do lineTo(p1) +} + +// helper +def show01(b: Bool) = if (b) { "1" } else { "0" } + +// # Transformations + +def translate(by: Vec2D){ body: => Unit / Path2D }: Unit / Path2D = { + try body() with Path2D { + def moveTo(p) = resume(do moveTo(Vec2D(p.x + by.x, p.y + by.y))) + def lineTo(p) = resume(do lineTo(Vec2D(p.x + by.x, p.y + by.y))) + def cubicBezierTo(c1, c2, t) = + resume(do cubicBezierTo(Vec2D(c1.x + by.x, c1.y + by.y), Vec2D(c2.x + by.x, c2.y + by.y), Vec2D(t.x + by.x, t.y + by.y))) + def quadraticBezierTo(c, t) = + resume(do quadraticBezierTo(Vec2D(c.x + by.x, c.y + by.y), Vec2D(t.x + by.x, t.y + by.y))) + def arc(rx, ry, rot, largeArc, sweep, to) = + resume(do arc(rx, ry, rot, largeArc, sweep, Vec2D(to.x + by.x, to.y + by.y))) + } +} +def rotate(center: Vec2D, angle: Double){ body: => Unit / Path2D }: Unit / Path2D = + try body() with Path2D { + def moveTo(p) = resume(do moveTo(rotatePoint(p, center, angle))) + def lineTo(p) = resume(do lineTo(rotatePoint(p, center, angle))) + def cubicBezierTo(c1, c2, t) = + resume(do cubicBezierTo(rotatePoint(c1, center, angle), rotatePoint(c2, center, angle), rotatePoint(t, center, angle))) + def quadraticBezierTo(c, t) = + resume(do quadraticBezierTo(rotatePoint(c, center, angle), rotatePoint(t, center, angle))) + def arc(rx, ry, rot, largeArc, sweep, to) = + resume(do arc(rx, ry, rot + angle, largeArc, sweep, rotatePoint(to, center, angle))) + } +def scale(factor: Double){ body: => Unit / Path2D }: Unit / Path2D = { + try body() with Path2D { + def moveTo(p) = resume(do moveTo(Vec2D(p.x * factor, p.y * factor))) + def lineTo(p) = resume(do lineTo(Vec2D(p.x * factor, p.y * factor))) + def cubicBezierTo(c1, c2, t) = + resume(do cubicBezierTo(Vec2D(c1.x * factor, c1.y * factor), Vec2D(c2.x * factor, c2.y * factor), Vec2D(t.x * factor, t.y * factor))) + def quadraticBezierTo(c, t) = + resume(do quadraticBezierTo(Vec2D(c.x * factor, c.y * factor), Vec2D(t.x * factor, t.y * factor))) + def arc(rx, ry, rot, largeArc, sweep, to) = + resume(do arc(rx * factor, ry * factor, rot, largeArc, sweep, Vec2D(to.x * factor, to.y * factor))) + } +} + +def noise(by: Double){ body: => Unit / Path2D }: Unit / Path2D = { + try body() with Path2D { + def moveTo(p) = resume(do moveTo(Vec2D(p.x + by * 2.0 * random() - by, p.y + by * 2.0 * random() - by))) + def lineTo(p) = resume(do lineTo(Vec2D(p.x + by * 2.0 * random() - by, p.y + by * 2.0 * random() - by))) + def cubicBezierTo(c1, c2, t) = + resume(do cubicBezierTo( + Vec2D(c1.x + by * 2.0 * random() - by, c1.y + by * 2.0 * random() - by), + Vec2D(c2.x + by * 2.0 * random() - by, c2.y + by * 2.0 * random() - by), + Vec2D(t.x + by * 2.0 * random() - by, t.y + by * 2.0 * random() - by) + )) + def quadraticBezierTo(c, t) = + resume(do quadraticBezierTo( + Vec2D(c.x + by * 2.0 * random() - by, c.y + by * 2.0 * random() - by), + Vec2D(t.x + by * 2.0 * random() - by, t.y + by * 2.0 * random() - by) + )) + def arc(rx, ry, rot, largeArc, sweep, to) = + resume(do arc(rx, ry, rot, largeArc, sweep, Vec2D(to.x + by * 2.0 * random() - by, to.y + by * 2.0 * random() - by))) + } +} + +// # Rendering + +def asSvgPath{ body: => Unit / Path2D }: String = stringBuffer { + var first = true + try { + try body() with Path2D { + def moveTo(p) = resume(do emit("M ${p.x.show} ${p.y.show}")) + def lineTo(p) = resume(do emit("L ${p.x.show} ${p.y.show}")) + def cubicBezierTo(c1, c2, t) = + resume(do emit("C ${c1.x.show} ${c1.y.show}, ${c2.x.show} ${c2.y.show}, ${t.x.show} ${t.y.show}")) + def quadraticBezierTo(c, t) = + resume(do emit("Q ${c.x.show} ${c.y.show}, ${t.x.show} ${t.y.show}")) + def arc(rx, ry, rot, largeArc, sweep, to) = + resume(do emit("A ${rx.show} ${ry.show} ${rot.show} ${largeArc.show01} ${sweep.show01} ${to.x.show} ${to.y.show}")) + } + } with emit[String] { s => + if (not(first)) { do write(" ") } + do write(s) + first = false + resume(()) + } + do flush() +} + + + +def asPgfPath{ body: => Unit / Path2D }: String = stringBuffer { + var first = true + var current = None() + try { + try body() with Path2D { + def moveTo(p) = { + current = p + resume(do emit("\\pgfpathmoveto{\\pgfpoint{${p.x.show}cm}{${p.y.show}cm}}")) + } + def lineTo(p) = { + current = p + resume(do emit("\\pgfpathlineto{\\pgfpoint{${p.x.show}cm}{${p.y.show}cm}}")) + } + def cubicBezierTo(c1, c2, t) = { + current = p + resume(do emit("\\pgfpathcurveto{\\pgfpoint{${c1.x.show}cm}{${c1.y.show}cm}}{\\pgfpoint{${c2.x.show}cm}{${c2.y.show}cm}}{\\pgfpoint{${t.x.show}cm}{${t.y.show}cm}}")) + } + def quadraticBezierTo(c, t) = { + current = p + resume(do emit("\\pgfpathquadraticcurveto{\\pgfpoint{${c.x.show}cm}{${c.y.show}cm}}{\\pgfpoint{${t.x.show}cm}{${t.y.show}cm}}")) + } + def arc(rx, ry, xAxisRotation, largeArcFlag, sweepFlag, Vec2D(endX, endY)) = { + val startX = current.x + val startY = current.y + + // Ensure radii are positive and non-zero + val rxPos = if (rx <= 0.0) 0.0001 else rx + val ryPos = if (ry <= 0.0) 0.0001 else ry + + // Convert rotation from degrees to radians + val phi = xAxisRotation * math.PI / 180.0 + + // Step 1: Transform to origin + val cosPhi = math.cos(phi) + val sinPhi = math.sin(phi) + + // Step 1.1: Compute (x1', y1') + val x1p = cosPhi * (startX - endX) / 2.0 + sinPhi * (startY - endY) / 2.0 + val y1p = -sinPhi * (startX - endX) / 2.0 + cosPhi * (startY - endY) / 2.0 + + // Step 2: Compute center + // Correction of radii + val lambda = (x1p * x1p) / (rxPos * rxPos) + (y1p * y1p) / (ryPos * ryPos) + + val rxFinal = if (lambda > 1.0) math.sqrt(lambda) * rxPos else rxPos + val ryFinal = if (lambda > 1.0) math.sqrt(lambda) * ryPos else ryPos + + // Step 2.1: Compute (cx', cy') + val sign = if (largeArcFlag == sweepFlag) -1.0 else 1.0 + val factor = sign * math.sqrt( + math.max(0.0, ((rxFinal * rxFinal * ryFinal * ryFinal) - (rxFinal * rxFinal * y1p * y1p) - (ryFinal * ryFinal * x1p * x1p)) / + ((rxFinal * rxFinal * y1p * y1p) + (ryFinal * ryFinal * x1p * x1p))) + ) + + val cxp = factor * rxFinal * y1p / ryFinal + val cyp = -factor * ryFinal * x1p / rxFinal + + // Step 3: Transform back to original coordinate space + val cx = cosPhi * cxp - sinPhi * cyp + (startX + endX) / 2.0 + val cy = sinPhi * cxp + cosPhi * cyp + (startY + endY) / 2.0 + + // Step 4: Calculate angles + // Vector from center to start point in transformed space + val startVectorX = (x1p - cxp) / rxFinal + val startVectorY = (y1p - cyp) / ryFinal + + // Vector from center to end point in transformed space + val endVectorX = (-x1p - cxp) / rxFinal + val endVectorY = (-y1p - cyp) / ryFinal + + // Calculate start angle + var startAngle = angleBetween(Vec2D(1.0, 0.0), Vec2D(startVectorX, startVectorY)) + + // Calculate angle extent + var deltaAngle = angleBetween(Vec2D(startVectorX, startVectorY), Vec2D(endVectorX, endVectorY)) + + // Adjust angle extent based on sweep and large arc flags + if (!sweepFlag && deltaAngle > 0.0) { + deltaAngle -= 2.0 * math.PI + } else if (sweepFlag && deltaAngle < 0.0) { + deltaAngle += 2.0 * math.PI + } + + // Convert to degrees for PGF + val startAngleDeg = (startAngle * 180.0 / math.PI) % 360.0 + val endAngleDeg = (startAngle + deltaAngle) * 180.0 / math.PI % 360.0 + + resume(do emit(s"\\pgfpatharc{${startAngleDeg.show}}{${endAngleDeg.show}}{${rxFinal.show}cm and ${ryFinal.show}cm}")) + } + } + } with emit[String] { s => + if (not(first)) { do write(" ") } + do write(s) + first = false + resume(()) + } + do flush() +} \ No newline at end of file diff --git a/libraries/common/vec.effekt b/libraries/common/vec.effekt new file mode 100644 index 000000000..b2a55d5a7 --- /dev/null +++ b/libraries/common/vec.effekt @@ -0,0 +1,96 @@ +// Basic 2d and 3d vectors of doubles + +record Vec2D(x: Double, y: Double) +record Vec3D(x: Double, y: Double, z: Double) + +def infixAdd(a: Vec2D, b: Vec2D): Vec2D = Vec2D(a.x + b.x, a.y + b.y) +def infixSub(a: Vec2D, b: Vec2D): Vec2D = Vec2D(a.x - b.x, a.y - b.y) +def infixMul(f: Double, b: Vec2D): Vec2D = Vec2D(f * b.x, f * b.y) +def infixDiv(v: Vec2D, f: Double): Vec2D = Vec2D(v.x / f, v.y / f) +def infixAdd(a: Vec3D, b: Vec3D): Vec3D = Vec3D(a.x + b.x, a.y + b.y, a.z + b.z) +def infixSub(a: Vec3D, b: Vec3D): Vec3D = Vec3D(a.x - b.x, a.y - b.y, a.z - b.z) +def infixMul(f: Double, b: Vec3D): Vec3D = Vec3D(f * b.x, f * b.y, f * b.z) +def infixDiv(v: Vec3D, f: Double): Vec3D = Vec3D(v.x / f, v.y / f, v.z / f) + +def infixEq(a: Vec2D, b: Vec2D): Bool = a.x == b.x && a.y == b.y +def infixNeq(a: Vec2D, b: Vec2D): Bool = not(infixEq(a, b)) + +def infixEq(a: Vec3D, b: Vec3D): Bool = a.x == b.x && a.y == b.y && a.z == b.z +def infixNeq(a: Vec3D, b: Vec3D): Bool = not(infixEq(a, b)) + +def scalarProduct(a: Vec3D, b: Vec3D): Double = a.x * b.x + a.y * b.y + a.z * b.z +def vectorProduct(a: Vec3D, b: Vec3D): Vec3D = Vec3D( + a.y * b.z - a.z * b.y, + a.z * b.x - a.x * b.z, + a.x * b.y - a.y * b.x +) + +def scalarProduct(a: Vec2D, b: Vec2D): Double = a.x * b.x + a.y * b.y +def vectorProduct(a: Vec2D, b: Vec2D): Double = a.x * b.y - a.y * b.x + +def magnitude(v: Vec2D): Double = sqrt(v.x * v.x + v.y * v.y) +def magnitude(v: Vec3D): Double = sqrt(v.x * v.x + v.y * v.y + v.z * v.z) + +def normalize(v: Vec2D): Vec2D = { + val mag = magnitude(v) + Vec2D(v.x / mag, v.y / mag) +} + +def normalize(v: Vec3D): Vec3D = { + val mag = magnitude(v) + Vec3D(v.x / mag, v.y / mag, v.z / mag) +} + +def projection(a: Vec2D, b: Vec2D): Vec2D = { + val scalar = scalarProduct(a, b) / scalarProduct(b, b) + infixMul(scalar, b) +} + +def projection(a: Vec3D, b: Vec3D): Vec3D = { + val scalar = scalarProduct(a, b) / scalarProduct(b, b) + infixMul(scalar, b) +} + +def rotate(v: Vec2D, angle: Double): Vec2D = { + val s = sin(angle) + val c = cos(angle) + Vec2D(v.x * c - v.y * s, v.x * s + v.y * c) +} + +def rotate(v: Vec3D, axis: Vec3D, angle: Double): Vec3D = { + val s = sin(angle) + val c = cos(angle) + val dot = scalarProduct(axis, v) + val cross = vectorProduct(axis, v) + c * v + s * cross + (dot * (1.0 - c)) * axis +} + +def rotatePoint(p: Vec2D, center: Vec2D, angle: Double): Vec2D = { + val s = sin(angle) + val c = cos(angle) + val translatedX = p.x - center.x + val translatedY = p.y - center.y + val rotatedX = translatedX * c - translatedY * s + val rotatedY = translatedX * s + translatedY * c + Vec2D(rotatedX + center.x, rotatedY + center.y) +} + +def angleBetween(a: Vec2D, b: Vec2D): Double = { + val dot = scalarProduct(a, b) + val magA = magnitude(a) + val magB = magnitude(b) + acos(dot / (magA * magB)) +} + +def angleBetween(a: Vec3D, b: Vec3D): Double = { + val dot = scalarProduct(a, b) + val magA = magnitude(a) + val magB = magnitude(b) + acos(dot / (magA * magB)) +} + +extern pure def atan2(y: Double, x: Double): Double = + js "Math.atan2(${y}, ${x})" + chez "(atan ${y} ${x})" + vm "effekt::atan2(Double, Double)" + llvm "%z = call %Double @atan2(double ${y}, double ${x}) ret %Double %z" \ No newline at end of file