-
-
Save lihaoyi/9443f8e0ecc68d1058ad to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
object ScalaJSExample extends js.JSApp{ | |
def main() = { | |
val xs = Seq(1, 2, 3) | |
println(xs.toString) | |
val ys = Seq(4, 5, 6) | |
println(ys.toString) | |
val zs = for{ | |
x <- xs | |
y <- ys | |
} yield x * y | |
println(zs.toString) | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import scala.util.Random | |
case class Point(x: Double, y: Double){ | |
def +(p: Point) = Point(x + p.x, y + p.y) | |
def -(p: Point) = Point(x - p.x, y - p.y) | |
def *(d: Double) = Point(x * d, y * d) | |
def /(d: Double) = Point(x / d, y / d) | |
def length = Math.sqrt(x * x + y * y) | |
} | |
class Enemy(var pos: Point, var vel: Point) | |
object ScalaJSExample extends js.JSApp{ | |
import Page._ | |
var startTime = js.Date.now() | |
var player = Point(dom.innerWidth.toInt/2, dom.innerHeight.toInt/2) | |
var enemies = Seq.empty[Enemy] | |
var death: Option[(String, Int)] = None | |
def run() = { | |
enemies = enemies.filter(e => | |
e.pos.x >= 0 && e.pos.x <= canvas.width && | |
e.pos.y >= 0 && e.pos.y <= canvas.height | |
) | |
def randSpeed = Random.nextInt(5) - 3 | |
enemies = enemies ++ Seq.fill(20 - enemies.length)( | |
new Enemy( | |
Point(Random.nextInt(canvas.width.toInt), 0), | |
Point(randSpeed, randSpeed) | |
) | |
) | |
for(enemy <- enemies){ | |
enemy.pos = enemy.pos + enemy.vel | |
val delta = player - enemy.pos | |
enemy.vel = enemy.vel + delta / delta.length / 100 | |
} | |
if(enemies.exists(e => (e.pos - player).length < 20)){ | |
death = Some((s"You lasted $deltaT seconds", 100)) | |
enemies = enemies.filter(e => (e.pos - player).length > 20) | |
} | |
} | |
def deltaT = ((js.Date.now() - startTime) / 1000).toInt | |
def draw() = { | |
renderer.clearRect(0, 0, canvas.width, canvas.height) | |
death match{ | |
case None => | |
renderer.fillStyle = "white" | |
renderer.fillRect(player.x - 10, player.y - 10, 20, 20) | |
renderer.fillText("player", player.x - 15, player.y - 30) | |
renderer.fillStyle = "red" | |
for (enemy <- enemies){ | |
renderer.fillRect(enemy.pos.x - 10, enemy.pos.y - 10, 20, 20) | |
} | |
renderer.fillStyle = "white" | |
renderer.fillText(s"$deltaT seconds", canvas.width / 2 - 100, canvas.height / 5) | |
case Some((msg, time)) => | |
renderer.fillStyle = "white" | |
renderer.fillText(msg, canvas.width / 2 - 100, canvas.height / 2) | |
if (time - 1 == 0){ | |
death = None | |
startTime = js.Date.now() | |
}else{ | |
death = Option((msg, time - 1)) | |
} | |
} | |
} | |
def main() = { | |
dom.document.onmousemove = { (e: dom.MouseEvent) => | |
player = Point(e.clientX.toInt, e.clientY.toInt) | |
(): js.Any | |
} | |
dom.setInterval(() => {run(); draw()}, 20) | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* A spiritual clone of Flappy Bird. Click to jump, don't hit the white barriers! | |
* | |
* High scores are saved using HTML5 Local Storage. | |
*/ | |
object ScalaJSExample extends js.JSApp{ | |
import Page._ | |
var game = new Game() | |
def main() = { | |
dom.setInterval(() => game.run(), 20 * 800 / canvas.width) | |
canvas.onclick = { (e: dom.MouseEvent) => | |
game.v += canvas.height / 200 | |
} | |
} | |
class Game{ | |
renderer.fillStyle = "black" | |
renderer.fillRect(0, 0, canvas.width, canvas.height) | |
var i = 0.0 | |
var h = canvas.height / 2.0 | |
var v = 0.0 | |
val a = -0.05 | |
def run() = { | |
val imageData = renderer.getImageData(1, 0, canvas.width-1, canvas.height) | |
clear() | |
println("Score: " + i.toDouble.toInt) | |
val highScore = Option(dom.localStorage.getItem("highScore")).fold(0)(_.toString.toDouble.toInt) | |
println("High Score: " + highScore) | |
i += 1 | |
dom.localStorage.setItem("highScore", Math.max(i, highScore).toString) | |
v += a | |
h += v | |
val spotData = renderer.getImageData(canvas.width/2 + 3, canvas.height - h, 1, 1).data | |
if (spotData.take(3).map(_.toInt).sum != 0 || h > canvas.height || h < 0) { | |
game = new Game() | |
} else{ | |
renderer.putImageData(imageData, 0, 0) | |
renderer.fillStyle = "black" | |
renderer.fillRect(canvas.width-1, 0, 1, canvas.height) | |
if (i.toInt % (canvas.width / 3).toInt == 0){ | |
val gapHeight = canvas.height / 5 | |
val gap = util.Random.nextInt(canvas.height.toInt - gapHeight.toInt) | |
renderer.fillStyle = "white" | |
renderer.fillRect(canvas.width-2, 0, 2, gap) | |
renderer.fillRect(canvas.width-2, gap + gapHeight, 2, canvas.height - gap - gapHeight) | |
} | |
val r = (h / canvas.height * 255).toInt | |
val b = (255 / (1 + Math.pow(Math.E, -v))).toInt | |
renderer.fillStyle = s"rgb($r, 255, $b)" | |
renderer.fillRect(canvas.width / 2, canvas.height - h, 2, 2) | |
} | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Simple, zero-dependency, self-contained JSON parser, serializer and AST | |
* taken from | |
* | |
* https://github.com/nestorpersist/json | |
* | |
* Notably, does not use the browser's native JSON capability, but re-implements | |
* it all in order to be 100% compatible with both Scala-JS and Scala-JVM | |
*/ | |
object ScalaJSExample extends js.JSApp{ | |
def main() = { | |
val parsed = Json.read(ugly) | |
println(parsed(0).toString) | |
println(parsed(8)("real").toString) | |
println(parsed(8)("comment").toString) | |
println(parsed(8)("jsontext").toString) | |
println(Json.write(parsed(8)).toString) | |
} | |
val ugly = | |
""" | |
|[ | |
| "JSON Test Pattern pass1", | |
| {"object with 1 member":["array with 1 element"]}, | |
| {}, | |
| [], | |
| -42, | |
| true, | |
| false, | |
| null, | |
| { | |
| "integer": 1234567890, | |
| "real": -9876.543210, | |
| "e": 0.123456789e-12, | |
| "E": 1.234567890E+34, | |
| "": 23456789012E66, | |
| "zero": 0, | |
| "one": 1, | |
| "space": " ", | |
| "quote": "\"", | |
| "backslash": "\\", | |
| "controls": "\b\f\n\r\t", | |
| "slash": "/ & \/", | |
| "alpha": "abcdefghijklmnopqrstuvwyz", | |
| "ALPHA": "ABCDEFGHIJKLMNOPQRSTUVWYZ", | |
| "digit": "0123456789", | |
| "0123456789": "digit", | |
| "special": "`1~!@#$%^&*()_+-={':[,]}|;.</>?", | |
| "hex": "\u0123\u4567\u89AB\uCDEF\uabcd\uef4A", | |
| "true": true, | |
| "false": false, | |
| "null": null, | |
| "array":[ ], | |
| "object":{ }, | |
| "address": "50 St. James Street", | |
| "url": "http://www.JSON.org/", | |
| "comment": "// /* <!-- --", | |
| "# -- --> */": " ", | |
| " s p a c e d " :[1,2 , 3 | |
| | |
|, | |
| | |
|4 , 5 , 6 ,7 ],"compact":[1,2,3,4,5,6,7], | |
| "jsontext": "{\"object with 1 member\":[\"array with 1 element\"]}", | |
| "quotes": "" \u005Cu0022 %22 0x22 034 "", | |
| "\/\\\"\uCAFE\uBABE\uAB98\uFCDE\ubcda\uef4A\b\f\n\r\t`1~!@#$%^&*()_+-=[]{}|;:',./<>?" | |
|: "A key can be any string" | |
| }, | |
| 0.5 ,98.6 | |
|, | |
|99.44 | |
|, | |
| | |
|1066, | |
|1e1, | |
|0.1e1, | |
|1e-1, | |
|1e00,2e+00,2e-00 | |
|,"rosebud"] | |
""".stripMargin | |
} | |
object Js { | |
sealed trait Value{ | |
def value: Any | |
def apply(i: Int): Value = this.asInstanceOf[Array].value(i) | |
def apply(s: java.lang.String): Value = this.asInstanceOf[Object].value.find(_._1 == s).get._2 | |
} | |
case class String(value: java.lang.String) extends Value | |
case class Object(value: Seq[(java.lang.String, Value)]) extends Value | |
case class Array(value: Seq[Value]) extends Value | |
case class Number(value: java.lang.String) extends Value | |
case object False extends Value{ | |
def value = true | |
} | |
case object True extends Value{ | |
def value = false | |
} | |
case object Null extends Value{ | |
def value = null | |
} | |
} | |
object Json { | |
def write(v: Js.Value): String = v match { | |
case Js.String(s) => | |
val out = s.flatMap { | |
case '\\' => "\\\\" | |
case '"' => "\\\"" | |
case '/' => "\\/" | |
case '\b' => "\\b" | |
case '\t' => "\\t" | |
case '\n' => "\\n" | |
case '\f' => "\\f" | |
case '\r' => "\\r" | |
case c if c < ' ' => | |
val t = "000" + Integer.toHexString(c) | |
"\\u" + t.takeRight(4) | |
case c => c.toString | |
} | |
'"' + out + '"' | |
case Js.Object(kv) => | |
val contents = kv.toIterator.map{ | |
case (k, v) => s"${write(Js.String(k))}: ${write(v)}" | |
}.mkString(", ") | |
s"{$contents}" | |
case Js.Array(vs) => s"[${vs.map(write).mkString(", ")}]" | |
case Js.Number(d) => d | |
case Js.False => "false" | |
case Js.True => "true" | |
case Js.Null => "null" | |
} | |
/** | |
* Self-contained JSON parser adapted from | |
* | |
* https://github.com/nestorpersist/json | |
*/ | |
def read(s: String): Js.Value = { | |
// *** Character Kinds | |
type CharKind = Int | |
val Letter = 0 | |
val Digit = 1 | |
val Minus = 2 | |
val Quote = 3 | |
val Colon = 4 | |
val Comma = 5 | |
val Lbra = 6 | |
val Rbra = 7 | |
val Larr = 8 | |
val Rarr = 9 | |
val Blank = 10 | |
val Other = 11 | |
val Eof = 12 | |
val Slash = 13 | |
// *** Token Kinds | |
type TokenKind = Int | |
val ID = 0 | |
val STRING = 1 | |
val NUMBER = 2 | |
val BIGNUMBER = 3 | |
val FLOATNUMBER = 4 | |
val COLON = 5 | |
val COMMA = 6 | |
val LOBJ = 7 | |
val ROBJ = 8 | |
val LARR = 9 | |
val RARR = 10 | |
val BLANK = 11 | |
val EOF = 12 | |
// *** Character => CharKind Map *** | |
val charKind = (0 to 255).toArray.map { | |
case c if 'a'.toInt <= c && c <= 'z'.toInt => Letter | |
case c if 'A'.toInt <= c && c <= 'Z'.toInt => Letter | |
case c if '0'.toInt <= c && c <= '9'.toInt => Digit | |
case '-' => Minus | |
case ',' => Comma | |
case '"' => Quote | |
case ':' => Colon | |
case '{' => Lbra | |
case '}' => Rbra | |
case '[' => Larr | |
case ']' => Rarr | |
case ' ' => Blank | |
case '\t' => Blank | |
case '\n' => Blank | |
case '\r' => Blank | |
case '/' => Slash | |
case _ => Other | |
} | |
// *** Character Escapes | |
val escapeMap = Map[Int, String]( | |
'\\'.toInt -> "\\", | |
'/'.toInt -> "/", | |
'\"'.toInt -> "\"", | |
'b'.toInt -> "\b", | |
'f'.toInt -> "\f", | |
'n'.toInt -> "\n", | |
'r'.toInt -> "\r", | |
't'.toInt -> "\t" | |
) | |
// *** Import Shared Data *** | |
// *** INPUT STRING *** | |
// array faster than accessing string directly using charAt | |
//final val s1 = s.toCharArray() | |
val size = s.size | |
// *** CHARACTERS *** | |
var pos = 0 | |
var ch: Int = 0 | |
var chKind: CharKind = 0 | |
var chLinePos: Int = 0 | |
var chCharPos: Int = 0 | |
def chNext() = { | |
if (pos < size) { | |
//ch = s1(pos).toInt | |
ch = s.charAt(pos) | |
chKind = if (ch < 255) { | |
charKind(ch) | |
} else { | |
Other | |
} | |
pos += 1 | |
if (ch == '\n'.toInt) { | |
chLinePos += 1 | |
chCharPos = 1 | |
} else { | |
chCharPos += 1 | |
} | |
} else { | |
ch = -1 | |
pos = size + 1 | |
chKind = Eof | |
} | |
} | |
def chError(msg: String): Nothing = { | |
throw new Json.Exception(msg, s, chLinePos, chCharPos) | |
} | |
def chMark = pos - 1 | |
def chSubstr(first: Int, delta: Int = 0) = { | |
s.substring(first, pos - 1 - delta) | |
} | |
// *** LEXER *** | |
var tokenKind = BLANK | |
var tokenValue = "" | |
var linePos = 1 | |
var charPos = 1 | |
def getDigits() = { | |
while (chKind == Digit) chNext() | |
} | |
def handleDigit() { | |
val first = chMark | |
getDigits() | |
val k1 = if (ch == '.'.toInt) { | |
chNext() | |
getDigits() | |
BIGNUMBER | |
} else { | |
NUMBER | |
} | |
val k2 = if (ch == 'E'.toInt || ch == 'e'.toInt) { | |
chNext() | |
if (ch == '+'.toInt) { | |
chNext() | |
} else if (ch == '-'.toInt) { | |
chNext() | |
} | |
getDigits() | |
FLOATNUMBER | |
} else { | |
k1 | |
} | |
tokenKind = k2 | |
tokenValue = chSubstr(first) | |
} | |
def handleRaw() { | |
chNext() | |
val first = chMark | |
var state = 0 | |
do { | |
if (chKind == Eof) chError("EOF encountered in raw string") | |
state = (ch, state) match { | |
case ('}', _) => 1 | |
case ('"', 1) => 2 | |
case ('"', 2) => 3 | |
case ('"', 3) => 0 | |
case _ => 0 | |
} | |
chNext() | |
} while (state != 3) | |
tokenKind = STRING | |
tokenValue = chSubstr(first, 3) | |
} | |
def handle(i: Int) = { | |
chNext() | |
tokenKind = i | |
tokenValue = "" | |
} | |
def tokenNext() { | |
do { | |
linePos = chLinePos | |
charPos = chCharPos | |
val kind: Int = chKind | |
kind match { | |
case Letter => | |
val first = chMark | |
while (chKind == Letter || chKind == Digit) { | |
chNext() | |
} | |
tokenKind = ID | |
tokenValue = chSubstr(first) | |
case Digit => handleDigit() | |
case Minus => | |
chNext() | |
handleDigit() | |
tokenValue = "-" + tokenValue | |
case Quote => | |
val sb = new StringBuilder(50) | |
chNext() | |
var first = chMark | |
while (ch != '"'.toInt && ch >= 32) { | |
if (ch == '\\'.toInt) { | |
sb.append(chSubstr(first)) | |
chNext() | |
escapeMap.get(ch) match { | |
case Some(s) => | |
sb.append(s) | |
chNext() | |
case None => | |
if (ch != 'u'.toInt) chError("Illegal escape") | |
chNext() | |
var code = 0 | |
for (i <- 1 to 4) { | |
val ch1 = ch.toChar.toString | |
val i = "0123456789abcdef".indexOf(ch1.toLowerCase) | |
if (i == -1) chError("Illegal hex character") | |
code = code * 16 + i | |
chNext() | |
} | |
sb.append(code.toChar.toString) | |
} | |
first = chMark | |
} else { | |
chNext() | |
} | |
} | |
if (ch != '"') chError("Unexpected string character: " + ch.toChar) | |
sb.append(chSubstr(first)) | |
tokenKind = STRING | |
tokenValue = sb.toString() | |
chNext() | |
if (tokenValue.length() == 0 && ch == '{') { | |
handleRaw() | |
} | |
case Colon => handle(COLON) | |
case Comma => handle(COMMA) | |
case Lbra => handle(LOBJ) | |
case Rbra => handle(ROBJ) | |
case Larr => handle(LARR) | |
case Rarr => handle(RARR) | |
case Blank => | |
do chNext() while (chKind == Blank) | |
tokenKind = BLANK | |
tokenValue = "" | |
case Other => chError("Unexpected character: " + ch.toChar + " " + ch) | |
case Eof => | |
chNext() | |
tokenKind = EOF | |
tokenValue = "" | |
case Slash => | |
if (chKind != Slash) chError("Expecting Slash") | |
do chNext() while (ch != '\n' && chKind != Eof) | |
tokenKind = BLANK | |
tokenValue = "" | |
} | |
} while (tokenKind == BLANK) | |
} | |
def tokenError(msg: String): Nothing = { | |
throw new Json.Exception(msg, s, linePos, charPos) | |
} | |
// *** PARSER *** | |
def handleEof() = tokenError("Unexpected eof") | |
def handleUnexpected(i: String) = tokenError(s"Unexpected input: [$i]") | |
def handleArray(): Js.Array = { | |
tokenNext() | |
var result = List.empty[Js.Value] | |
while (tokenKind != RARR) { | |
result = getJson() :: result | |
tokenKind match{ | |
case COMMA => tokenNext() | |
case RARR => // do nothing | |
case _ => tokenError("Expecting , or ]") | |
} | |
} | |
tokenNext() | |
Js.Array(result.reverse) | |
} | |
def handleObject(): Js.Object = { | |
tokenNext() | |
var result = List.empty[(String, Js.Value)] | |
while (tokenKind != ROBJ) { | |
if (tokenKind != STRING && tokenKind != ID) tokenError("Expecting string or name") | |
val name = tokenValue | |
tokenNext() | |
if (tokenKind != COLON) tokenError("Expecting :") | |
tokenNext() | |
result = (name -> getJson()) :: result | |
tokenKind match{ | |
case COMMA => tokenNext() | |
case ROBJ => // do nothing | |
case _ => tokenError("Expecting , or }") | |
} | |
} | |
tokenNext() | |
Js.Object(result.reverse) | |
} | |
def handleNumber(name: String, f: String => Unit) = { | |
val v = try { | |
f(tokenValue) | |
} catch { | |
case _: Throwable => tokenError("Bad " + name) | |
} | |
val old = tokenValue | |
tokenNext() | |
Js.Number(old) | |
} | |
def getJson(): Js.Value = { | |
val kind: Int = tokenKind | |
val result: Js.Value = kind match { | |
case ID => | |
val result = tokenValue match { | |
case "true" => Js.True | |
case "false" => Js.False | |
case "null" => Js.Null | |
case _ => tokenError("Not true, false, or null") | |
} | |
tokenNext() | |
result | |
case STRING => | |
val result = tokenValue | |
tokenNext() | |
Js.String(result) | |
case NUMBER => handleNumber("NUMBER", _.toLong) | |
case BIGNUMBER => handleNumber("BIGNUMBER", _.toDouble) | |
case FLOATNUMBER => handleNumber("FLOATNUMBER", _.toDouble) | |
case COLON => handleUnexpected(":") | |
case COMMA => handleUnexpected(",") | |
case LOBJ => handleObject() | |
case ROBJ => handleUnexpected("}") | |
case LARR => handleArray() | |
case RARR => handleUnexpected("]") | |
case EOF => handleEof() | |
} | |
result | |
} | |
def parse(): Js.Value = { | |
chNext() | |
tokenNext() | |
val result = getJson | |
if (tokenKind != EOF) tokenError("Excess input") | |
result | |
} | |
parse() | |
} | |
class Exception(val msg: String, | |
val input: String, | |
val line: Int, | |
val char: Int) | |
extends scala.Exception(s"JsonParse Error: $msg line $line [$char] in $input") | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import scalatags.JsDom.all._ | |
/** | |
* Splash page for www.scala-js-fiddle.com. Uses Scalatags to render a HTML page | |
* containing the text on the page, while using a HTML5 Canvas element to slowly | |
* render a Sierpinski Triangle behind the text pixel by pixel. | |
*/ | |
object ScalaJSExample extends js.JSApp{ | |
def main() = { | |
Page.clear() | |
println( | |
div( | |
padding:="10px", | |
id:="page", | |
h1( | |
marginTop:="0px", | |
img(src:="/Shield.svg", height:="40px", marginBottom:="-10px"), | |
" Scala.jsFiddle" | |
), | |
p( | |
"Scala.jsFiddle is an online scratchpad to try out your ", | |
a(href:="http://www.scala-js.org/", "Scala.js "), | |
"snippets. Enter Scala code on the left and see the results on the right, ", | |
"after the Scala is compiled to Javascript using the Scala.js compiler." | |
), | |
ul( | |
li(yellow("Ctrl/Cmd-Enter"), ": compile and execute"), | |
li(yellow("Ctrl/Cmd-Shift-Enter"), ": compile and execute optimized code"), | |
li(yellow("Ctrl/Cmd-Space"), ": autocomplete at caret"), | |
li(yellow("Ctrl/Cmd-J"), ": show the generated Javascript code"), | |
li(yellow("Ctrl/Cmd-Shift-J"), ": show the generated code, optimized by Google Closure"), | |
li(yellow("Ctrl/Cmd-S"), ": save the code to a public Gist"), | |
li(yellow("Ctrl/Cmd-E"), ": export your code to a stand-alone web page") | |
), | |
p( | |
"To load the code from an existing gist, simply go to " | |
), | |
ul(li(green(s"${Page.fiddleUrl}/gist/<gist-id>"))), | |
p( | |
"Where ",green("<gist-id>"), " is the last section of the gist's URL.", | |
"You can also go to:" | |
), | |
ul(li(green(s"${Page.fiddleUrl}/gist/<gist-id>/<file-name>"))), | |
p( | |
"If you want a particular file in a multi file gist." | |
), | |
p( | |
"If you need ideas of things you can make using Scala.jsFiddle, check ", | |
"out some of our examples:" | |
), | |
ul( | |
for { | |
(file, name) <- Seq( | |
"BasicOperations.scala" -> "Basic Operations", | |
"SierpinskiTriangle.scala" -> "Sierpinski Triangle", | |
"Turmites.scala" -> "Turmites", | |
"Oscilloscope.scala" -> "Oscilloscope", | |
"FlappyLine.scala" -> "Flappy Line", | |
"SquareRoot.scala" -> "Square Root solver", | |
"DodgeTheDots.scala" -> "Dodge the Dots", | |
"SpaceInvaders.scala" -> "Space Invaders", | |
"SquareRootRx.scala" -> "Square Root solver with Scala.rx", | |
"TodoMVC.scala" -> "TodoMVC", | |
"JsonParser.scala" -> "JSON Parser", | |
// "ScalaAsyncPaintbrush.scala" -> "Scala Async Paintbrush", | |
"RayTracer.scala" -> "RayTracer" | |
) | |
} yield li( | |
a(href:=(dom.document.location.origin + s"/gist/${Page.fiddleGistId}/" + file), name) | |
) | |
), | |
p("Scala-Js-Fiddle comes with the following default imports at the top of every program:"), | |
pre(Page.fiddlePrelude), | |
p( | |
"This imports the Scala.js ", blue("js"), " and ", blue("dom"), " namespaces ", | |
"ready to use, imports the ", blue("Page"), " object and imports the ", | |
blue("scalatags.JsDom"), " namespace to support the helper functions in ", | |
blue("Page"), "." | |
), | |
p(blue("Page"), " provides the following built-in variables to give you started quickly:"), | |
ul( | |
li( | |
green("canvas"), ": a ", blue("dom.HTMLCanvasElement"), " that lets you draw ", | |
"on the right pane" | |
), | |
li( | |
green("renderer"), ": a ", blue("dom.CanvasRenderingContext2D"), " that ", | |
"lets you draw on the ", blue("canvas") | |
), | |
li( | |
green("print/println(s: Frag*)"), ": prints the given values to the right pane as ", | |
"a scalatags ", blue("Frag"), "s. These could be ", blue("String"), "s, numbers, or ", | |
"Scalatags ", blue("Tag"), "s. This behaves slightly differently from the built-in ", | |
blue("println"), " which is available as ", blue("Predef.println"), " and prints to ", | |
"the browser console" | |
), | |
li(green("clear()"), ": Removes all printed output from the right pane"), | |
li( | |
green("scroll(px: Int)"), ": Scrolls the right pane up or down by ", | |
"the given number of pixels" | |
), | |
li( | |
"The colors ", red("red"), ", ", green("green"), ", ", blue("blue"), | |
", ", yellow("yellow"), " and ", orange("orange"), | |
" to help prettify your output" | |
) | |
), | |
p("Apart from these inbuilt helpers, available libraries include:"), | |
ul( | |
li(a(href:="https://github.com/lihaoyi/scalatags", "scalatags")), | |
li(a(href:="https://github.com/lihaoyi/scala.rx", "scala.rx")), | |
li(a(href:="https://github.com/scala-js/scala-js-dom", "scala-js-dom")), | |
li(a(href:="https://github.com/scala/async", "scala-async")) | |
), | |
p( | |
"Scala.jsFiddle is made by ", a(href:="https://github.com/lihaoyi")("Li Haoyi"), | |
" and can be found on ", a(href:="https://github.com/lihaoyi/scala-js-fiddle")("Github") | |
) | |
) | |
) | |
// scroll to top so if the users screen is too short, he sees the top of the | |
// rendered page rather than the bottom | |
Page.scroll(-1000) | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Math._ | |
object ScalaJSExample extends js.JSApp{ | |
def main(): Unit = { | |
val (h, w) = (Page.canvas.height, Page.canvas.width) | |
var x = 0.0 | |
val graphs = Seq[(String, Double => Double)]( | |
("red", sin), | |
("green", x => 2 - abs(x % 8 - 4)), | |
("blue", x => 3 * pow(sin(x / 12), 2) * sin(x)) | |
).zipWithIndex | |
dom.setInterval(() => { | |
x = (x + 1) % w | |
if (x == 0) Page.renderer.clearRect(0, 0, w, h) | |
else for (((color, func), i) <- graphs) { | |
val y = func(x/w * 75) * h/40 + h/3 * (i+0.5) | |
Page.renderer.fillStyle = color | |
Page.renderer.fillRect(x, y, 3, 3) | |
} | |
}, 10) | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import math._ | |
import scala.scalajs.concurrent.JSExecutionContext.Implicits.queue | |
import ScalaJSExample.{Color, Epsilon} | |
import scala.language.postfixOps | |
/** | |
* A simple ray tracer, taken from the PyPy benchmarks | |
* | |
* https://bitbucket.org/pypy/benchmarks/src/846fa56a282b/own/raytrace-simple.py?at=default | |
* | |
* Half the lines of code | |
*/ | |
object ScalaJSExample extends js.JSApp{ | |
import Page._ | |
val Epsilon = 0.00001 | |
type Color = Vec | |
val Color = Vec | |
def main() = { | |
val r = new util.Random(16314302) | |
val spiral = for (i <- 0 until 11) yield { | |
val theta = i * (i + 5) * Pi / 100 + 0.3 | |
val center = (0 - 4 * sin(theta), 1.5 - i / 2.0, 0 - 4 * cos(theta)) | |
val form = Sphere(center, 0.3 + i * 0.1) | |
val surface = Flat((i / 6.0, 1 - i / 6.0, 0.5)) | |
(form, surface) | |
} | |
def rand(d: Double) = (r.nextDouble() - 0.5) * d * 2 | |
val drops = Array( | |
Sphere((2.5, 2.5, -8), 0.3), | |
Sphere((1.5, 2.2, -7), 0.25), | |
Sphere((-1.3, 0.8, -8.5), 0.15), | |
Sphere((0.5, -2.5, -7.5), 0.2), | |
Sphere((-1.8, 2.3, -7.5), 0.3), | |
Sphere((-1.8, -2.3, -7.5), 0.3), | |
Sphere((1.3, 0.0, -8), 0.25) | |
).map(_ -> Refractor()) | |
val s = new Scene( | |
objects = Array( | |
Sphere((0, 0, 0), 2) -> Flat((1, 1, 1), specularC = 0.6, lambertC = 0.4), | |
Plane((0, 4, 0), (0, 1, 0)) -> Checked(), | |
Plane((0, -4, 0), (0, 1, 0)) -> Flat((0.9, 1, 1)), | |
Plane((6, 0, 0), (1, 0, 0)) -> Flat((1, 0.9, 1)), | |
Plane((-6, 0, 0), (1, 0, 0)) -> Flat((1, 1, 0.9)), | |
Plane((0, 0, 6), (0, 0, 1)) -> Flat((0.9, 0.9, 1)) | |
) ++ spiral ++ drops, | |
lightPoints = Array( | |
Light((0, -3, 0), (3, 3, 0)), | |
Light((3, 3, 0), (0, 3, 3)), | |
Light((-3, 3, 0), (3, 0, 3)) | |
), | |
position = (0, 0, -15), | |
lookingAt = (0, 0, 0), | |
fieldOfView = 45.0 | |
) | |
val c = new Canvas{ | |
val width = math.min(canvas.width.toInt, canvas.height.toInt) | |
val height = math.min(canvas.width.toInt, canvas.height.toInt) | |
val data = renderer.getImageData(0, 0, canvas.width, canvas.height) | |
def save(y: Int): Unit = { | |
renderer.putImageData(data, 0, 0, 0, y-1, width, 1) | |
} | |
def plot(x: Int, y: Int, rgb: ScalaJSExample.Color): Unit = { | |
val index = (y * data.width + x) * 4 | |
data.data(index+0) = (rgb.x * 255).toInt | |
data.data(index+1) = (rgb.y * 255).toInt | |
data.data(index+2) = (rgb.z * 255).toInt | |
data.data(index+3) = 255 | |
} | |
} | |
s.render(c) | |
} | |
} | |
final case class Vec(x: Double, y: Double, z: Double){ | |
def magnitude = sqrt(this dot this) | |
def +(o: Vec) = Vec(x + o.x, y + o.y, z + o.z) | |
def -(o: Vec) = Vec(x - o.x, y - o.y, z - o.z) | |
def *(o: Vec) = Vec(x * o.x, y * o.y, z * o.z) | |
def *(f: Double) = Vec(x * f, y * f, z * f) | |
def /(f: Double) = Vec(x / f, y / f, z / f) | |
def dot(o: Vec) = x * o.x + y * o.y + z * o.z | |
def cross(o: Vec) = Vec( | |
y * o.z - z * o.y, | |
z * o.x - x * o.z, | |
x * o.y - y * o.x | |
) | |
def normalized = this / magnitude | |
def reflectThrough(normal: Vec) = this - normal * (this dot normal) * 2 | |
} | |
object Vec{ | |
case class Unit(x: Double, y: Double, z: Double) | |
implicit def normalizer(v: Vec) = { | |
val l = v.magnitude | |
new Unit(v.x / l, v.y / l, v.z / l) | |
} | |
implicit def denormalizer(v: Vec.Unit) = new Vec(v.x, v.y, v.z) | |
implicit def pointify[X: Numeric, Y: Numeric, Z: Numeric](x: (X, Y, Z)): Vec = Vec( | |
implicitly[Numeric[X]].toDouble(x._1), | |
implicitly[Numeric[Y]].toDouble(x._2), | |
implicitly[Numeric[Z]].toDouble(x._3) | |
) | |
implicit def pointify2[X: Numeric, Y: Numeric, Z: Numeric](x: (X, Y, Z)): Vec.Unit = Vec.normalizer(x) | |
} | |
abstract class Form{ | |
def intersectionTime(ray: Ray): Double | |
def normalAt(p: Vec): Vec | |
} | |
case class Sphere(center: Vec, radius: Double) extends Form{ | |
def intersectionTime(ray: Ray) = { | |
val cp = center - ray.point | |
val v = cp dot ray.vector | |
val d = radius * radius - ((cp dot cp) - v * v) | |
if (d < 0) -1 | |
else v - sqrt(d) | |
} | |
def normalAt(p: Vec) = (p - center).normalized | |
} | |
case class Plane(point: Vec, normal: Vec.Unit) extends Form{ | |
def intersectionTime(ray: Ray) = { | |
val v = ray.vector dot normal | |
if (v != 0) ((point - ray.point) dot normal) / v | |
else -1 | |
} | |
def normalAt(p: Vec) = normal | |
} | |
case class Ray(point: Vec, vector: Vec.Unit){ | |
def pointAtTime(t: Double) = point + vector * t | |
} | |
case class Light(center: Vec, color: Color) | |
abstract class Surface{ | |
def colorAt(scene: Scene, ray: Ray, p: Vec, normal: Vec.Unit, depth: Int): Color | |
} | |
abstract class SolidSurface extends Surface{ | |
def baseColorAt(p: Vec): Color | |
def specularC: Double | |
def lambertC: Double | |
val ambientC = 1.0 - specularC - lambertC | |
def colorAt(scene: Scene, ray: Ray, p: Vec, normal: Vec.Unit, depth: Int): Color = { | |
val b = baseColorAt(p) | |
val specular = { | |
val reflectedRay = Ray(p, ray.vector.reflectThrough(normal)) | |
val reflectedColor = scene.rayColor(reflectedRay, depth) | |
reflectedColor * specularC | |
} | |
val lambert = { | |
var lambertAmount = Vec(0, 0, 0) | |
for (i <- 0 until scene.lightPoints.length) { | |
val light = scene.lightPoints(i) | |
if (scene.lightIsVisible(light.center, p)) { | |
val d = p - light.center | |
val dLength = d.magnitude | |
val contribution = light.color * abs(d dot normal / (dLength * dLength)) | |
lambertAmount += contribution | |
} | |
} | |
b * lambertAmount * lambertC | |
} | |
val ambient = b * ambientC | |
specular + lambert + ambient | |
} | |
} | |
case class Refractor(refractiveIndex: Double = 0.5) extends Surface{ | |
def colorAt(scene: Scene, ray: Ray, p: Vec, normal: Vec.Unit, depth: Int): Color = { | |
val r = if ((normal dot ray.vector) < 0) | |
refractiveIndex | |
else | |
1.0 / refractiveIndex | |
val c = (normal * -1) dot ray.vector | |
val sqrtValue = 1 - r * r * (1 - c * c) | |
if (sqrtValue > 0){ | |
val refracted = ray.vector * r + normal * (r * c - sqrt(sqrtValue)) | |
scene.rayColor(Ray(p, refracted), depth) | |
}else{ | |
val perp = ray.vector dot normal | |
val reflected: Vec = Vec.denormalizer(ray.vector) + normal * 2 * perp | |
scene.rayColor(Ray(p, reflected), depth) | |
} | |
} | |
} | |
case class Flat(baseColor: Color = Color(1, 1, 1), | |
specularC: Double = 0.3, | |
lambertC: Double = 0.6) extends SolidSurface{ | |
def baseColorAt(p: Vec) = baseColor | |
} | |
case class Checked(baseColor: Color = Color(1, 1, 1), | |
specularC: Double = 0.3, | |
lambertC: Double = 0.6, | |
otherColor: Color = (0, 0, 0), | |
checkSize: Double = 1) extends SolidSurface{ | |
override def baseColorAt(p: Vec) = { | |
val v = p * (1.0 / checkSize) | |
def f(x: Double) = (abs(x) + 0.5).toInt | |
if ((f(v.x) + f(v.y) + f(v.z)) % 2 == 1) otherColor | |
else baseColor | |
} | |
} | |
abstract class Canvas{ | |
def width: Int | |
def height: Int | |
def save(y: Int): Unit | |
def plot(x: Int, y: Int, rgb: Color) | |
} | |
class Scene(objects: Array[(Form, Surface)], | |
val lightPoints: Array[Light], | |
position: Vec, | |
lookingAt: Vec, | |
fieldOfView: Double){ | |
def lightIsVisible(l: Vec, p: Vec) = { | |
val ray = Ray(p, l - p) | |
val length = (l - p).magnitude | |
var visible = true | |
for (i <- 0 until objects.length){ | |
val (o, s) = objects(i) | |
val t = o.intersectionTime(ray) | |
if (t > Epsilon && t < length - Epsilon){ | |
visible = false | |
} | |
} | |
visible | |
} | |
def rayColor(ray: Ray, depth: Int): Color = { | |
if (depth > 3) (0, 0, 0) | |
else{ | |
var (minT, minO, minS) = (-1.0, null: Form, null: Surface) | |
for(i <- 0 until objects.length){ | |
val (o, s) = objects(i) | |
val t = o.intersectionTime(ray) | |
if (t > Epsilon && (t < minT || minT < 0)){ | |
minT = t | |
minO = o | |
minS = s | |
} | |
} | |
minT match{ | |
case -1 => (0, 0, 0) | |
case t => | |
val p = ray.pointAtTime(minT) | |
minS.colorAt(this, ray, p, minO.normalAt(p), depth + 1) | |
} | |
} | |
} | |
def render(canvas: Canvas) = { | |
val fovRadians = Pi * (fieldOfView / 2.0) / 180.0 | |
val halfWidth = tan(fovRadians) | |
val halfHeight = halfWidth | |
val width = halfWidth * 2 | |
val height= halfHeight * 2 | |
val pixelWidth = width / (canvas.width - 1) | |
val pixelHeight = height / (canvas.height - 1) | |
val eye = Ray(position, lookingAt - position) | |
val vpRight = eye.vector.cross((0, 1, 0)).normalized | |
val vpUp = vpRight.cross(eye.vector).normalized | |
var y = 0; | |
lazy val interval: Int = dom.setInterval({ () => | |
for (x <- 0 until canvas.width){ | |
val xcomp = vpRight * (x * pixelWidth - halfWidth) | |
val ycomp = vpUp * (y * pixelHeight - halfHeight) | |
val ray = Ray(eye.point, xcomp + ycomp + eye.vector) | |
val color = rayColor(ray, 0) | |
canvas.plot(x, y, color) | |
} | |
canvas.save(y) | |
if (y > canvas.height) dom.clearInterval(interval) | |
y+= 1 | |
}, 0) | |
interval | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import org.scalajs.dom | |
import concurrent._ | |
import async.Async._ | |
import scalajs.concurrent.JSExecutionContext.Implicits.queue | |
/** | |
* Re-implementation of the mouse-drag example from Deprecating the Observer | |
* Pattern | |
* | |
* http://lampwww.epfl.ch/~imaier/pub/DeprecatingObserversTR2010.pdf | |
* | |
* Using Scala.Async instead of Scala.React and continuations. Click and drag | |
* around the canvas on the right to see pretty shapes appear. | |
*/ | |
object ScalaJSExample extends js.JSApp{ | |
import Page._ | |
def main() = { | |
val command = Channel[dom.MouseEvent]() | |
canvas.onmousemove = command.update _ | |
canvas.onmouseup = command.update _ | |
canvas.onmousedown = command.update _ | |
renderer.lineWidth = 5 | |
renderer.strokeStyle = "red" | |
renderer.fillStyle = "cyan" | |
val flow = async{ | |
while(true){ | |
renderer.beginPath() | |
val start = await(command.filter(_.`type` == "mousedown")()) | |
renderer.moveTo(start.clientX - canvas.width, start.clientY) | |
var m = await(command()) | |
while(m.`type` == "mousemove"){ | |
renderer.lineTo(m.clientX - canvas.width, m.clientY) | |
renderer.stroke() | |
m = await(command()) | |
} | |
renderer.fill() | |
} | |
} | |
} | |
} | |
case class Channel[T](){ | |
private[this] var value: Promise[T] = null | |
def apply(): Future[T] = { | |
value = Promise[T]() | |
value.future | |
} | |
def update(t: T) = { | |
if (value != null && !value.isCompleted) value.success(t) | |
} | |
def filter(p: T => Boolean) = { | |
val filtered = Channel[T]() | |
async{ | |
while(true){ | |
val t = await(this()) | |
if (p(t)) filtered() = t | |
} | |
} | |
filtered | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
case class Pt(x: Double, y: Double) | |
object ScalaJSExample extends js.JSApp{ | |
println("Hello!!") | |
val corners = Seq( | |
Pt(Page.canvas.width/2, 0), | |
Pt(0, Page.canvas.height), | |
Pt(Page.canvas.width, Page.canvas.height) | |
) | |
var p = corners(0) | |
val (w, h) = (Page.canvas.height.toDouble, Page.canvas.height.toDouble) | |
def main() = { | |
dom.setInterval(() => for(_ <- 0 until 10){ | |
val c = corners(util.Random.nextInt(3)) | |
p = Pt((p.x + c.x) / 2, (p.y + c.y) / 2) | |
val m = (p.y / h) | |
val r = 255 - (p.x / w * m * 255).toInt | |
val g = 255 - ((w-p.x) / w * m * 255).toInt | |
val b = 255 - ((h - p.y) / h * 255).toInt | |
Page.renderer.fillStyle = s"rgb($r, $g, $b)" | |
Page.renderer.fillRect(p.x, p.y, 1, 1) | |
}, 10) | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
object ScalaJSExample extends js.JSApp{ | |
def main(): Unit = { | |
var i = 0 | |
dom.setInterval(() => for(_ <- 0 until 5){ | |
i += 1 | |
val x = i % canvas.width | |
val y = Math.sin(i / canvas.width * 10) * canvas.height / 4 + canvas.height / 2 | |
output.clear() | |
val r = math.abs(((y / canvas.height - 0.5) * 255 * 4).toInt) | |
println(x) | |
println(y) | |
renderer.fillStyle = s"rgb($r, 255, 255)" | |
renderer.fillRect(x, y, 2, 2) | |
}, 10) | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
case class Point(x: Double, y: Double){ | |
def +(p: Point) = Point(x + p.x, y + p.y) | |
def -(p: Point) = Point(x - p.x, y - p.y) | |
def /(d: Double) = Point(x / d, y / d) | |
def *(d: Double) = Point(x * d, y * d) | |
def length = Math.sqrt(x * x + y * y) | |
} | |
object ScalaJSExample extends js.JSApp{ | |
import Page._ | |
var count = 0 | |
var player = Point(dom.innerWidth / 2, dom.innerHeight / 2) | |
val corners = Seq(Point(255, 255), Point(0, 255), Point(128, 0)) | |
var bullets = Seq.empty[Point] | |
var enemies = Seq.empty[Point] | |
var wave = 1 | |
def run = { | |
count += 1 | |
bullets = bullets.map( | |
p => Point(p.x, p.y - 5) | |
) | |
if (enemies.isEmpty){ | |
enemies = for{ | |
x <- (0 until canvas.width.toInt by 50) | |
y <- 0 until wave | |
} yield { | |
Point(x, 50 + y * 50) | |
} | |
wave += 1 | |
} | |
enemies = enemies.filter( e => | |
!bullets.exists(b => | |
(e - b).length < 5 | |
) | |
) | |
enemies = enemies.map{ e => | |
val i = count % 200 | |
if (i < 50) e.copy(x = e.x - 0.2) | |
else if (i < 100) e.copy(y = e.y + 0.2) | |
else if (i < 150) e.copy(x = e.x + 0.2) | |
else e.copy(y = e.y + 0.2) | |
} | |
if (keysDown(38)) player += Point(0, -2) | |
if (keysDown(37)) player += Point(-2, 0) | |
if (keysDown(39)) player += Point(2, 0) | |
if (keysDown(40)) player += Point(0, 2) | |
} | |
def draw = { | |
renderer.clearRect(0, 0, canvas.width, canvas.height) | |
renderer.fillStyle = "white" | |
renderer.fillRect(player.x - 5, player.y - 5, 10, 10) | |
renderer.fillStyle = "yellow" | |
for (enemy <- enemies){ | |
renderer.fillRect(enemy.x - 5, enemy.y - 5, 10, 10) | |
} | |
renderer.fillStyle = "red" | |
for (bullet <- bullets){ | |
renderer.fillRect(bullet.x - 2, bullet.y - 2, 4, 4) | |
} | |
} | |
val keysDown = collection.mutable.Set.empty[Int] | |
def main() = { | |
dom.onkeypress = {(e: dom.KeyboardEvent) => | |
if (e.keyCode.toInt == 32) bullets = player +: bullets | |
} | |
dom.onkeydown = {(e: dom.KeyboardEvent) => | |
keysDown.add(e.keyCode.toInt) | |
} | |
dom.onkeyup = {(e: dom.KeyboardEvent) => | |
keysDown.remove(e.keyCode.toInt) | |
} | |
dom.setInterval(() => {run; draw}, 20) | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* A simple square root solver, one step at a time | |
*/ | |
object ScalaJSExample extends js.JSApp{ | |
def main() = { | |
val n = 1000.0 | |
var guess = 1.0 | |
lazy val id: Int = dom.setInterval(() => { | |
val newGuess = guess - (guess * guess - n) / (2 * guess) | |
if (newGuess == guess) dom.clearInterval(id) | |
else guess = newGuess | |
println(guess) | |
}, 1000) | |
id | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* A simple Scala.Rx dataflow graph which looks like: | |
* | |
* guess -> error | |
* ^ | | |
* | v | |
* --------o | |
* | |
* Where the changes propagate around and around until | |
* the magnitude of `error` drops below `epsilon` | |
* | |
*/ | |
object ScalaJSExample extends js.JSApp{ | |
def main() = { | |
import rx._ | |
val n = 10.0 | |
val guess = Var(1.0) | |
val epsilon = 0.0000000001 | |
val error = Rx{ guess() * guess() - n } | |
val o = Obs(error){ | |
println("Guess: " + guess()) | |
if (math.abs(error()) > epsilon) { | |
guess() = guess() - error() / (2 * guess()) | |
} | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Port of the Scala.js TodoMVC example application | |
* | |
* http://lihaoyi.github.io/workbench-example-app/todo.html | |
* | |
* To Scala.jsFiddle. Mostly involved sticking the two source files into one | |
* (since Scala.jsFiddle doesn't support multiple files) and inlining/tweaking | |
* the CSS so it looks right | |
*/ | |
import org.scalajs.dom | |
import rx._ | |
import scala.collection.{SortedMap, mutable} | |
import scalatags.JsDom.tags2 | |
import scalatags.JsDom.tags2.section | |
import scala.util.{Failure, Success, Random} | |
import rx.core.{Propagator, Obs} | |
import org.scalajs.dom.{Element, DOMParser} | |
/** | |
* A minimal binding between Scala.Rx and Scalatags and Scala-Js-Dom | |
*/ | |
object Framework { | |
/** | |
* Wraps reactive strings in spans, so they can be referenced/replaced | |
* when the Rx changes. | |
*/ | |
implicit def RxStr[T](r: Rx[T])(implicit f: T => Frag): Modifier = { | |
rxMod(Rx(span(r()))) | |
} | |
/** | |
* Sticks some Rx into a Scalatags fragment, which means hooking up an Obs | |
* to propagate changes into the DOM via the element's ID. Monkey-patches | |
* the Obs onto the element itself so we have a reference to kill it when | |
* the element leaves the DOM (e.g. it gets deleted). | |
*/ | |
implicit def rxMod(r: Rx[HtmlTag]): Modifier = { | |
def rSafe = r.toTry match { | |
case Success(v) => v.render | |
case Failure(e) => span(e.toString, backgroundColor := "red").render | |
} | |
var last = rSafe | |
Obs(r, skipInitial = true){ | |
val newLast = rSafe | |
last.parentElement.replaceChild(newLast, last) | |
last = newLast | |
} | |
last | |
} | |
implicit def RxAttrValue[T: AttrValue] = new AttrValue[Rx[T]]{ | |
def apply(t: Element, a: Attr, r: Rx[T]): Unit = { | |
Obs(r){ implicitly[AttrValue[T]].apply(t, a, r())} | |
} | |
} | |
implicit def RxStyleValue[T: StyleValue] = new StyleValue[Rx[T]]{ | |
def apply(t: Element, s: Style, r: Rx[T]): Unit = { | |
Obs(r){ implicitly[StyleValue[T]].apply(t, s, r())} | |
} | |
} | |
} | |
case class Task(txt: Var[String], done: Var[Boolean]) | |
object ScalaJSExample extends js.JSApp{ | |
import Framework._ | |
val editing = Var[Option[Task]](None) | |
val tasks = Var( | |
Seq( | |
Task(Var("TodoMVC Task A"), Var(true)), | |
Task(Var("TodoMVC Task B"), Var(false)), | |
Task(Var("TodoMVC Task C"), Var(false)) | |
) | |
) | |
val filter = Var("All") | |
val filters = Map[String, Task => Boolean]( | |
("All", t => true), | |
("Active", !_.done()), | |
("Completed", _.done()) | |
) | |
val done = Rx{tasks().count(_.done())} | |
val notDone = Rx{tasks().length - done()} | |
val inputBox = input( | |
id:="new-todo", | |
placeholder:="What needs to be done?", | |
autofocus:=true | |
).render | |
def main() = { | |
println( | |
div( | |
tags2.style(inlineCss), | |
section(id:="todoapp", color:="black")( | |
header(id:="header")( | |
h1("todos"), | |
form( | |
inputBox, | |
onsubmit := { () => | |
tasks() = Task(Var(inputBox.value), Var(false)) +: tasks() | |
inputBox.value = "" | |
false | |
} | |
) | |
), | |
section(id:="main")( | |
input( | |
id:="toggle-all", | |
`type`:="checkbox", | |
cursor:="pointer", | |
onclick := { () => | |
val target = tasks().exists(_.done() == false) | |
Var.set(tasks().map(_.done -> target): _*) | |
} | |
), | |
label(`for`:="toggle-all", "Mark all as complete"), | |
Rx { | |
ul(id := "todo-list")( | |
for (task <- tasks() if filters(filter())(task)) yield { | |
val inputRef = input(`class` := "edit", value := task.txt()).render | |
li( | |
`class` := Rx{ | |
if (task.done()) "completed" | |
else if (editing() == Some(task)) "editing" | |
else "" | |
}, | |
div(`class` := "view")( | |
"ondblclick".attr := { () => | |
editing() = Some(task) | |
}, | |
input( | |
`class` := "toggle", | |
`type` := "checkbox", | |
cursor := "pointer", | |
onchange := { () => | |
task.done() = !task.done() | |
}, | |
if (task.done()) checked := true | |
), | |
label(task.txt()), | |
button( | |
`class` := "destroy", | |
cursor := "pointer", | |
onclick := { () =>tasks() = tasks().filter(_ != task) } | |
) | |
), | |
form( | |
onsubmit := { () => | |
task.txt() = inputRef.value | |
editing() = None | |
false | |
}, | |
inputRef | |
) | |
) | |
} | |
) | |
}, | |
footer(id:="footer")( | |
span(id:="todo-count")(strong(RxStr(notDone)(intFrag)), " item left"), | |
ul(id:="filters")( | |
for ((name, pred) <- filters.toSeq) yield { | |
li(a( | |
`class`:=Rx{ | |
if(name == filter()) "selected" | |
else "" | |
}, | |
name, | |
href:="#", | |
onclick := {() => filter() = name} | |
)) | |
} | |
), | |
button( | |
id:="clear-completed", | |
onclick := { () => tasks() = tasks().filter(!_.done()) }, | |
"Clear completed (", done, ")" | |
) | |
) | |
), | |
footer(id:="info")( | |
p("Double-click to edit a todo"), | |
p(a(href:="https://github.com/lihaoyi/workbench-example-app/blob/todomvc/src/main/scala/example/ScalaJSExample.scala")("Source Code")), | |
p("Created by ", a(href:="http://github.com/lihaoyi")("Li Haoyi")) | |
) | |
) | |
).render | |
) | |
} | |
def inlineCss = """ | |
html, | |
body { | |
margin: 0; | |
padding: 0; | |
} | |
button { | |
margin: 0; | |
padding: 0; | |
border: 0; | |
background: none; | |
font-size: 100%; | |
vertical-align: baseline; | |
font-family: inherit; | |
color: inherit; | |
-webkit-appearance: none; | |
-ms-appearance: none; | |
-o-appearance: none; | |
appearance: none; | |
} | |
body{ | |
background: url('https://raw.github.com/lihaoyi/workbench-example-app/todomvc/src/main/resources/css/bg.png'); | |
} | |
#sandbox { | |
font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif; | |
line-height: 1.4em; | |
background: url('https://raw.github.com/lihaoyi/workbench-example-app/todomvc/src/main/resources/css/bg.png'); | |
color: #4d4d4d; | |
width: 550px; | |
margin: 0 auto; | |
-webkit-font-smoothing: antialiased; | |
-moz-font-smoothing: antialiased; | |
-ms-font-smoothing: antialiased; | |
-o-font-smoothing: antialiased; | |
font-smoothing: antialiased; | |
} | |
button, | |
input[type="checkbox"] { | |
outline: none; | |
} | |
#todoapp { | |
background: #fff; | |
background: rgba(255, 255, 255, 0.9); | |
margin: 130px 0 40px 0; | |
border: 1px solid #ccc; | |
position: relative; | |
border-top-left-radius: 2px; | |
border-top-right-radius: 2px; | |
box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 0.2), | |
0 25px 50px 0 rgba(0, 0, 0, 0.15); | |
} | |
#todoapp:before { | |
content: ''; | |
border-left: 1px solid #f5d6d6; | |
border-right: 1px solid #f5d6d6; | |
width: 2px; | |
position: absolute; | |
top: 0; | |
left: 40px; | |
height: 100%; | |
} | |
#todoapp input::-webkit-input-placeholder { | |
font-style: italic; | |
} | |
#todoapp input::-moz-placeholder { | |
font-style: italic; | |
color: #a9a9a9; | |
} | |
#todoapp h1 { | |
position: absolute; | |
top: -120px; | |
width: 100%; | |
font-size: 70px; | |
font-weight: bold; | |
text-align: center; | |
color: #b3b3b3; | |
color: rgba(255, 255, 255, 0.3); | |
text-shadow: -1px -1px rgba(0, 0, 0, 0.2); | |
-webkit-text-rendering: optimizeLegibility; | |
-moz-text-rendering: optimizeLegibility; | |
-ms-text-rendering: optimizeLegibility; | |
-o-text-rendering: optimizeLegibility; | |
text-rendering: optimizeLegibility; | |
} | |
#header { | |
padding-top: 15px; | |
border-radius: inherit; | |
} | |
#header:before { | |
content: ''; | |
position: absolute; | |
top: 0; | |
right: 0; | |
left: 0; | |
height: 15px; | |
z-index: 2; | |
border-bottom: 1px solid #6c615c; | |
background: #8d7d77; | |
background: -webkit-gradient(linear, left top, left bottom, from(rgba(132, 110, 100, 0.8)),to(rgba(101, 84, 76, 0.8))); | |
background: -webkit-linear-gradient(top, rgba(132, 110, 100, 0.8), rgba(101, 84, 76, 0.8)); | |
background: linear-gradient(top, rgba(132, 110, 100, 0.8), rgba(101, 84, 76, 0.8)); | |
filter: progid:DXImageTransform.Microsoft.gradient(GradientType=0,StartColorStr='#9d8b83', EndColorStr='#847670'); | |
border-top-left-radius: 1px; | |
border-top-right-radius: 1px; | |
} | |
#new-todo, | |
.edit { | |
position: relative; | |
margin: 0; | |
width: 100%; | |
font-size: 24px; | |
font-family: inherit; | |
line-height: 1.4em; | |
border: 0; | |
outline: none; | |
color: inherit; | |
padding: 6px; | |
border: 1px solid #999; | |
box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2); | |
-moz-box-sizing: border-box; | |
-ms-box-sizing: border-box; | |
-o-box-sizing: border-box; | |
box-sizing: border-box; | |
-webkit-font-smoothing: antialiased; | |
-moz-font-smoothing: antialiased; | |
-ms-font-smoothing: antialiased; | |
-o-font-smoothing: antialiased; | |
font-smoothing: antialiased; | |
} | |
#new-todo { | |
padding: 16px 16px 16px 60px; | |
border: none; | |
background: rgba(0, 0, 0, 0.02); | |
z-index: 100; | |
box-shadow: none; | |
} | |
#main { | |
position: relative; | |
z-index: 2; | |
border-top: 1px dotted #adadad; | |
} | |
label[for='toggle-all'] { | |
display: none; | |
} | |
#toggle-all { | |
position: absolute; | |
top: -42px; | |
left: -4px; | |
width: 40px; | |
text-align: center; | |
/* Mobile Safari */ | |
border: none; | |
} | |
#toggle-all:before { | |
content: '»'; | |
font-size: 28px; | |
color: #d9d9d9; | |
padding: 0 25px 7px; | |
} | |
#toggle-all:checked:before { | |
color: #737373; | |
} | |
#todo-list { | |
margin: 0; | |
padding: 0; | |
list-style: none; | |
} | |
#todo-list li { | |
position: relative; | |
font-size: 24px; | |
border-bottom: 1px dotted #ccc; | |
} | |
#todo-list li:last-child { | |
border-bottom: none; | |
} | |
#todo-list li.editing { | |
border-bottom: none; | |
padding: 0; | |
} | |
#todo-list li.editing .edit { | |
display: block; | |
width: 506px; | |
padding: 13px 17px 12px 17px; | |
margin: 0 0 0 43px; | |
} | |
#todo-list li.editing .view { | |
display: none; | |
} | |
#todo-list li .toggle { | |
text-align: center; | |
width: 40px; | |
/* auto, since non-WebKit browsers doesn't support input styling */ | |
height: auto; | |
position: absolute; | |
top: 0; | |
bottom: 0; | |
margin: auto 0; | |
/* Mobile Safari */ | |
border: none; | |
-webkit-appearance: none; | |
-ms-appearance: none; | |
-o-appearance: none; | |
appearance: none; | |
} | |
#todo-list li .toggle:after { | |
content: '✔'; | |
/* 40 + a couple of pixels visual adjustment */ | |
line-height: 43px; | |
font-size: 20px; | |
color: #d9d9d9; | |
text-shadow: 0 -1px 0 #bfbfbf; | |
} | |
#todo-list li .toggle:checked:after { | |
color: #85ada7; | |
text-shadow: 0 1px 0 #669991; | |
bottom: 1px; | |
position: relative; | |
} | |
#todo-list li label { | |
white-space: pre; | |
word-break: break-word; | |
padding: 15px 60px 15px 15px; | |
margin-left: 45px; | |
display: block; | |
line-height: 1.2; | |
-webkit-transition: color 0.4s; | |
transition: color 0.4s; | |
} | |
#todo-list li.completed label { | |
color: #a9a9a9; | |
text-decoration: line-through; | |
} | |
#todo-list li .destroy { | |
display: none; | |
position: absolute; | |
top: 0; | |
right: 10px; | |
bottom: 0; | |
width: 40px; | |
height: 40px; | |
margin: auto 0; | |
font-size: 22px; | |
color: #a88a8a; | |
-webkit-transition: all 0.2s; | |
transition: all 0.2s; | |
} | |
#todo-list li .destroy:hover { | |
text-shadow: 0 0 1px #000, | |
0 0 10px rgba(199, 107, 107, 0.8); | |
-webkit-transform: scale(1.3); | |
-ms-transform: scale(1.3); | |
transform: scale(1.3); | |
} | |
#todo-list li .destroy:after { | |
content: '✖'; | |
} | |
#todo-list li:hover .destroy { | |
display: block; | |
} | |
#todo-list li .edit { | |
display: none; | |
} | |
#todo-list li.editing:last-child { | |
margin-bottom: -1px; | |
} | |
#footer { | |
color: #777; | |
padding: 0 15px; | |
position: absolute; | |
right: 0; | |
bottom: -31px; | |
left: 0; | |
height: 20px; | |
text-align: center; | |
} | |
#footer:before { | |
content: ''; | |
position: absolute; | |
right: 0; | |
bottom: 31px; | |
left: 0; | |
height: 50px; | |
z-index: -1; | |
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.3), | |
0 6px 0 -3px rgba(255, 255, 255, 0.8), | |
0 7px 1px -3px rgba(0, 0, 0, 0.3), | |
0 43px 0 -6px rgba(255, 255, 255, 0.8), | |
0 44px 2px -6px rgba(0, 0, 0, 0.2); | |
} | |
#todo-count { | |
float: left; | |
text-align: left; | |
} | |
#filters { | |
margin: 0; | |
padding: 0; | |
list-style: none; | |
position: absolute; | |
right: 0; | |
left: 0; | |
} | |
#filters li { | |
display: inline; | |
} | |
#filters li a { | |
color: #83756f; | |
margin: 2px; | |
text-decoration: none; | |
} | |
#filters li a.selected { | |
font-weight: bold; | |
} | |
#clear-completed { | |
float: right; | |
position: relative; | |
line-height: 20px; | |
text-decoration: none; | |
background: rgba(0, 0, 0, 0.1); | |
font-size: 11px; | |
padding: 0 10px; | |
border-radius: 3px; | |
box-shadow: 0 -1px 0 0 rgba(0, 0, 0, 0.2); | |
} | |
#clear-completed:hover { | |
background: rgba(0, 0, 0, 0.15); | |
box-shadow: 0 -1px 0 0 rgba(0, 0, 0, 0.3); | |
} | |
#info { | |
margin: 65px auto 0; | |
color: #a6a6a6; | |
font-size: 12px; | |
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.7); | |
text-align: center; | |
} | |
#info a { | |
color: inherit; | |
} | |
/* | |
Hack to remove background from Mobile Safari. | |
Can't use it globally since it destroys checkboxes in Firefox and Opera | |
*/ | |
@media screen and (-webkit-min-device-pixel-ratio:0) { | |
#toggle-all, | |
#todo-list li .toggle { | |
background: none; | |
} | |
#todo-list li .toggle { | |
height: 40px; | |
} | |
#toggle-all { | |
top: -56px; | |
left: -15px; | |
width: 65px; | |
height: 41px; | |
-webkit-transform: rotate(90deg); | |
-ms-transform: rotate(90deg); | |
transform: rotate(90deg); | |
-webkit-appearance: none; | |
appearance: none; | |
} | |
} | |
.hidden { | |
display: none; | |
} | |
hr { | |
margin: 20px 0; | |
border: 0; | |
border-top: 1px dashed #C5C5C5; | |
border-bottom: 1px dashed #F7F7F7; | |
} | |
.learn a { | |
font-weight: normal; | |
text-decoration: none; | |
color: #b83f45; | |
} | |
.learn a:hover { | |
text-decoration: underline; | |
color: #787e7e; | |
} | |
.learn h3, | |
.learn h4, | |
.learn h5 { | |
margin: 10px 0; | |
font-weight: 500; | |
line-height: 1.2; | |
color: #000; | |
} | |
.learn h3 { | |
font-size: 24px; | |
} | |
.learn h4 { | |
font-size: 18px; | |
} | |
.learn h5 { | |
margin-bottom: 0; | |
font-size: 14px; | |
} | |
.learn ul { | |
padding: 0; | |
margin: 0 0 30px 25px; | |
} | |
.learn li { | |
line-height: 20px; | |
} | |
.learn p { | |
font-size: 15px; | |
font-weight: 300; | |
line-height: 1.3; | |
margin-top: 0; | |
margin-bottom: 0; | |
} | |
.quote { | |
border: none; | |
margin: 20px 0 60px 0; | |
} | |
.quote p { | |
font-style: italic; | |
} | |
.quote p:before { | |
content: '“'; | |
font-size: 50px; | |
opacity: .15; | |
position: absolute; | |
top: -20px; | |
left: 3px; | |
} | |
.quote p:after { | |
content: '”'; | |
font-size: 50px; | |
opacity: .15; | |
position: absolute; | |
bottom: -42px; | |
right: 3px; | |
} | |
.quote footer { | |
position: absolute; | |
bottom: -40px; | |
right: 0; | |
} | |
.quote footer img { | |
border-radius: 3px; | |
} | |
.quote footer a { | |
margin-left: 5px; | |
vertical-align: middle; | |
} | |
.speech-bubble { | |
position: relative; | |
padding: 10px; | |
background: rgba(0, 0, 0, .04); | |
border-radius: 5px; | |
} | |
.speech-bubble:after { | |
content: ''; | |
position: absolute; | |
top: 100%; | |
right: 30px; | |
border: 13px solid transparent; | |
border-top-color: rgba(0, 0, 0, .04); | |
} | |
.learn-bar > .learn { | |
position: absolute; | |
width: 272px; | |
top: 8px; | |
left: -300px; | |
padding: 10px; | |
border-radius: 5px; | |
background-color: rgba(255, 255, 255, .6); | |
-webkit-transition-property: left; | |
transition-property: left; | |
-webkit-transition-duration: 500ms; | |
transition-duration: 500ms; | |
} | |
@media (min-width: 899px) { | |
.learn-bar { | |
width: auto; | |
margin: 0 0 0 300px; | |
} | |
.learn-bar > .learn { | |
left: 8px; | |
} | |
.learn-bar #todoapp { | |
width: 550px; | |
margin: 130px auto 40px auto; | |
} | |
} | |
""" | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import scalatags.JsDom.all._ | |
case class Pt(x: Double, y: Double) | |
object ScalaJSExample extends js.JSApp{ | |
val directions = Seq( | |
Pt(0, 1), | |
Pt(1, 0), | |
Pt(0, -1), | |
Pt(-1, 0) | |
) | |
val (w, h) = (Page.canvas.width, Page.canvas.height) | |
var turmites = Seq( | |
Pt(w / 2, h / 2) -> 0 | |
) | |
def main() = { | |
dom.setInterval(() => for(_ <- 0 until 10){ | |
turmites = for((oldP, facing) <- turmites) yield { | |
val d = directions(facing) | |
val p = Pt((oldP.x + d.x + w) % w, (oldP.y + d.y + h) % h) | |
val data = Page.renderer.getImageData(p.x, p.y, 1, 1).data | |
val newFacing = if (data(0).toInt == 0){ | |
val m = (p.y / h) | |
val r = 255 - (p.x / w * m * 255).toInt | |
val g = 255 - ((w - p.x) / w * m * 255).toInt | |
val b = 255 - ((h - p.y) / h * 255).toInt | |
Page.renderer.fillStyle = s"rgb($r, $g, $b)" | |
(facing + 3) % 4 | |
}else{ | |
Page.renderer.fillStyle = "black" | |
(facing + 1) % 4 | |
} | |
Page.renderer.fillRect(p.x, p.y, 1, 1) | |
p -> newFacing | |
} | |
}, 20) | |
Page.canvas.onclick = { (e: dom.MouseEvent) => | |
val rect = Page.canvas.getBoundingClientRect() | |
val newP = Pt(e.clientX.toInt - rect.left, e.clientY.toInt - rect.top) | |
turmites = turmites :+ (newP -> util.Random.nextInt(4)) | |
} | |
} | |
println( | |
"An implementation of ", a(href:="http://en.wikipedia.org/wiki/Turmites")("Turmites"), | |
", in particular ", a(href:="http://en.wikipedia.org/wiki/Langton's_ant")("Langton's Ant"), | |
", on the HTML canvas. Click anywhere to create more turmites." | |
) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Dear @lihaoyi, I found this gist via scala-js-fiddle.com. I was wondering which gist to use. This one or the one it was forked from?