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("")
+}
+
+/// 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