diff --git a/examples/snek/README.md b/examples/snek/README.md
new file mode 100644
index 0000000000..88868a666c
--- /dev/null
+++ b/examples/snek/README.md
@@ -0,0 +1,17 @@
+# snek
+
+Snake game implemented using `gg` module.
+
+# Compiling & running
+
+## Compiling to binary
+```sh
+v -prod examples/snek/snek.v
+./examples/snek/snek # run snek game!
+```
+
+## Compiling to JS
+```sh
+v -b js_browser examples/snek/snek.js.v
+```
+And then open `examples/snek/index.html` in your favourite browser.
\ No newline at end of file
diff --git a/examples/snek/index.html b/examples/snek/index.html
new file mode 100644
index 0000000000..eb0c9de7da
--- /dev/null
+++ b/examples/snek/index.html
@@ -0,0 +1,6 @@
+
+ gg
+
+
+
+
\ No newline at end of file
diff --git a/examples/snek/snek.js.v b/examples/snek/snek.js.v
new file mode 100644
index 0000000000..ee347331a6
--- /dev/null
+++ b/examples/snek/snek.js.v
@@ -0,0 +1,197 @@
+import gg
+import gx
+// import sokol.sapp
+import time
+import rand
+
+// constants
+const (
+ top_height = 100
+ canvas_size = 700
+ game_size = 17
+ tile_size = canvas_size / game_size
+ tick_rate_ms = 100
+)
+
+// types
+struct Pos {
+ x int
+ y int
+}
+
+fn (a Pos) + (b Pos) Pos {
+ return Pos{a.x + b.x, a.y + b.y}
+}
+
+fn (a Pos) - (b Pos) Pos {
+ return Pos{a.x - b.x, a.y - b.y}
+}
+
+enum Direction {
+ up
+ down
+ left
+ right
+}
+
+struct App {
+mut:
+ gg &gg.Context
+ score int
+ snake []Pos
+ dir Direction
+ last_dir Direction
+ food Pos
+ start_time i64
+ last_tick i64
+}
+
+// utility
+fn (mut app App) reset_game() {
+ app.score = 0
+ app.snake = [
+ Pos{3, 8},
+ Pos{2, 8},
+ Pos{1, 8},
+ Pos{0, 8},
+ ]
+ app.dir = .right
+ app.last_dir = app.dir
+ app.food = Pos{10, 8}
+ app.start_time = time.ticks()
+ app.last_tick = time.ticks()
+}
+
+fn (mut app App) move_food() {
+ for {
+ x := rand.int_in_range(0, game_size)
+ y := rand.int_in_range(0, game_size)
+ app.food = Pos{x, y}
+
+ if app.food !in app.snake {
+ return
+ }
+ }
+}
+
+// events
+fn on_keydown(key gg.KeyCode, mod gg.Modifier, mut app App) {
+ match key {
+ .w, .up {
+ if app.last_dir != .down {
+ app.dir = .up
+ }
+ }
+ .s, .down {
+ if app.last_dir != .up {
+ app.dir = .down
+ }
+ }
+ .a, .left {
+ if app.last_dir != .right {
+ app.dir = .left
+ }
+ }
+ .d, .right {
+ if app.last_dir != .left {
+ app.dir = .right
+ }
+ }
+ else {}
+ }
+}
+
+fn on_frame(mut app App) {
+ app.gg.begin()
+
+ now := time.ticks()
+
+ if now - app.last_tick >= tick_rate_ms {
+ app.last_tick = now
+
+ // finding delta direction
+ delta_dir := match app.dir {
+ .up { Pos{0, -1} }
+ .down { Pos{0, 1} }
+ .left { Pos{-1, 0} }
+ .right { Pos{1, 0} }
+ }
+
+ // "snaking" along
+ mut prev := app.snake[0]
+ app.snake[0] = app.snake[0] + delta_dir
+
+ for i in 1 .. app.snake.len {
+ tmp := app.snake[i]
+ app.snake[i] = prev
+ prev = tmp
+ }
+
+ // adding last segment
+ if app.snake[0] == app.food {
+ app.move_food()
+ app.score++
+ /*
+ if app.score > app.best {
+ app.best = app.score
+ app.best.save()
+ }*/
+ app.snake << app.snake.last() + app.snake.last() - app.snake[app.snake.len - 2]
+ }
+
+ app.last_dir = app.dir
+ }
+ // drawing snake
+ for pos in app.snake {
+ app.gg.draw_rect(tile_size * pos.x, tile_size * pos.y + top_height, tile_size,
+ tile_size, gx.blue)
+ }
+
+ // drawing food
+ app.gg.draw_rect(tile_size * app.food.x, tile_size * app.food.y + top_height, tile_size,
+ tile_size, gx.red)
+
+ // drawing top
+ app.gg.draw_rect(0, 0, canvas_size, top_height, gx.black)
+ app.gg.draw_text(350, top_height / 2, 'Score: $app.score', gx.TextCfg{
+ color: gx.white
+ align: .center
+ vertical_align: .middle
+ size: 80
+ })
+
+ // checking if snake bit itself
+ if app.snake[0] in app.snake[1..] {
+ app.reset_game()
+ }
+ // checking if snake hit a wall
+ if app.snake[0].x < 0 || app.snake[0].x >= game_size || app.snake[0].y < 0
+ || app.snake[0].y >= game_size {
+ app.reset_game()
+ }
+
+ app.gg.end()
+}
+
+// setup
+fn main() {
+ mut app := App{
+ gg: &gg.Context{}
+ }
+ app.reset_game()
+
+ app.gg = gg.new_context(
+ bg_color: gx.white
+ frame_fn: on_frame
+ keydown_fn: on_keydown
+ user_data: &app
+ width: canvas_size
+ height: top_height + canvas_size
+ create_window: true
+ resizable: false
+ window_title: 'snek'
+ canvas: 'canvas'
+ )
+
+ app.gg.run()
+}
diff --git a/vlib/gg/gg.js.v b/vlib/gg/gg.js.v
index dedc7f202e..638193e12b 100644
--- a/vlib/gg/gg.js.v
+++ b/vlib/gg/gg.js.v
@@ -36,7 +36,7 @@ pub struct Event {
pub mut:
frame_count u64
typ DOMEventType
- key_code DOMKeyCode
+ key_code KeyCode
char_code u32
key_repeat bool
modifiers u32
@@ -252,7 +252,13 @@ pub:
enable_dragndrop bool // enable file dropping (drag'n'drop), default is false
max_dropped_files int = 1 // max number of dropped files to process (default: 1)
max_dropped_file_path_length int = 2048 // max length in bytes of a dropped UTF-8 file path (default: 2048)
- canvas JS.HTMLCanvasElement
+ canvas string
+}
+
+const size = Size{0, 0}
+
+pub fn window_size() Size {
+ return gg.size
}
pub struct Context {
@@ -284,9 +290,33 @@ pub mut:
pressed_keys [key_code_max]bool // an array representing all currently pressed keys
pressed_keys_edge [key_code_max]bool // true when the previous state of pressed_keys,
context JS.CanvasRenderingContext2D [noinit]
+ canvas JS.HTMLCanvasElement [noinit]
// *before* the current event was different
}
+fn get_canvas(elem JS.HTMLElement) &JS.HTMLCanvasElement {
+ match elem {
+ JS.HTMLCanvasElement {
+ return elem
+ }
+ else {
+ panic('gg: element is not an HTMLCanvasElement')
+ }
+ }
+}
+
+fn get_context(canvas JS.HTMLCanvasElement) JS.CanvasRenderingContext2D {
+ ctx := canvas.getContext('2d'.str, js_undefined()) or { panic('cannot get context') }
+ match ctx {
+ JS.CanvasRenderingContext2D {
+ return ctx
+ }
+ else {
+ panic('failed to get 2D context')
+ }
+ }
+}
+
pub fn new_context(cfg Config) &Context {
mut g := &Context{}
@@ -294,24 +324,30 @@ pub fn new_context(cfg Config) &Context {
g.width = cfg.width
g.height = cfg.height
g.ui_mode = cfg.ui_mode
+ mut sz := gg.size
+ sz.height = g.height
+ sz.width = g.width
g.config = cfg
if isnil(cfg.user_data) {
g.user_data = g
}
g.window = dom.window()
- ctx := cfg.canvas.getContext('2d'.str, js_undefined()) or { panic('') }
- match ctx {
- JS.CanvasRenderingContext2D {
- g.context = ctx
- }
- else {
- panic('gg: cannot get 2D context')
- }
+ document := dom.document
+ canvas_elem := document.getElementById(cfg.canvas.str) or {
+ panic('gg: cannot get canvas element')
}
+ canvas := get_canvas(canvas_elem)
+ g.canvas = canvas
+ g.context = get_context(g.canvas)
+
mouse_down_event_handler := fn [mut g] (event JS.Event) {
match event {
JS.MouseEvent {
- e := g.handle_mouse_event(event)
+ e := g.handle_mouse_event(event, .mouse_down)
+ if !isnil(g.config.event_fn) {
+ f := g.config.event_fn
+ f(e, g.config.user_data)
+ }
if !isnil(g.config.click_fn) {
f := g.config.click_fn
f(e.mouse_x, e.mouse_y, e.mouse_button, g.config.user_data)
@@ -324,7 +360,11 @@ pub fn new_context(cfg Config) &Context {
mouse_up_event_handler := fn [mut g] (event JS.Event) {
match event {
JS.MouseEvent {
- e := g.handle_mouse_event(event)
+ e := g.handle_mouse_event(event, .mouse_up)
+ if !isnil(g.config.event_fn) {
+ f := g.config.event_fn
+ f(e, g.config.user_data)
+ }
if !isnil(g.config.unclick_fn) {
f := g.config.unclick_fn
f(e.mouse_x, e.mouse_y, e.mouse_button, g.config.user_data)
@@ -336,7 +376,11 @@ pub fn new_context(cfg Config) &Context {
mouse_move_event_handler := fn [mut g] (event JS.Event) {
match event {
JS.MouseEvent {
- e := g.handle_mouse_event(event)
+ e := g.handle_mouse_event(event, .mouse_move)
+ if !isnil(g.config.event_fn) {
+ f := g.config.event_fn
+ f(e, g.config.user_data)
+ }
if !isnil(g.config.move_fn) {
f := g.config.move_fn
f(e.mouse_x, e.mouse_y, g.config.user_data)
@@ -349,7 +393,11 @@ pub fn new_context(cfg Config) &Context {
mouse_leave_event_handler := fn [mut g] (event JS.Event) {
match event {
JS.MouseEvent {
- e := g.handle_mouse_event(event)
+ e := g.handle_mouse_event(event, .mouse_leave)
+ if !isnil(g.config.event_fn) {
+ f := g.config.event_fn
+ f(e, g.config.user_data)
+ }
if !isnil(g.config.leave_fn) {
f := g.config.leave_fn
f(e, g.config.user_data)
@@ -362,7 +410,11 @@ pub fn new_context(cfg Config) &Context {
mouse_enter_event_handler := fn [mut g] (event JS.Event) {
match event {
JS.MouseEvent {
- e := g.handle_mouse_event(event)
+ e := g.handle_mouse_event(event, .mouse_enter)
+ if !isnil(g.config.event_fn) {
+ f := g.config.event_fn
+ f(e, g.config.user_data)
+ }
if !isnil(g.config.enter_fn) {
f := g.config.enter_fn
f(e, g.config.user_data)
@@ -371,11 +423,32 @@ pub fn new_context(cfg Config) &Context {
else {}
}
}
- cfg.canvas.addEventListener('mousedown'.str, mouse_down_event_handler, JS.EventListenerOptions{})
+
+ keydown_event_handler := fn [mut g] (event JS.Event) {
+ println('keyboard')
+ match event {
+ JS.KeyboardEvent {
+ e := g.handle_keyboard_event(event, .key_down)
+
+ if !isnil(g.config.event_fn) {
+ f := g.config.event_fn
+ f(e, g.config.user_data)
+ }
+ if !isnil(g.config.keydown_fn) {
+ f := g.config.keydown_fn
+ // todo: modifiers
+ f(e.key_code, .super, g.config.user_data)
+ }
+ }
+ else {}
+ }
+ }
+ g.canvas.addEventListener('mousedown'.str, mouse_down_event_handler, JS.EventListenerOptions{})
dom.window().addEventListener('mouseup'.str, mouse_up_event_handler, JS.EventListenerOptions{})
- cfg.canvas.addEventListener('mousemove'.str, mouse_move_event_handler, JS.EventListenerOptions{})
- cfg.canvas.addEventListener('mouseleave'.str, mouse_leave_event_handler, JS.EventListenerOptions{})
- cfg.canvas.addEventListener('mouseenter'.str, mouse_enter_event_handler, JS.EventListenerOptions{})
+ g.canvas.addEventListener('mousemove'.str, mouse_move_event_handler, JS.EventListenerOptions{})
+ g.canvas.addEventListener('mouseleave'.str, mouse_leave_event_handler, JS.EventListenerOptions{})
+ g.canvas.addEventListener('mouseenter'.str, mouse_enter_event_handler, JS.EventListenerOptions{})
+ dom.document.addEventListener('keydown'.str, keydown_event_handler, JS.EventListenerOptions{})
return g
}
@@ -400,6 +473,9 @@ pub fn (mut ctx Context) draw_line(x1 f32, y1 f32, x2 f32, y2 f32, c gx.Color) {
ctx.context.closePath()
}
+pub fn (mut ctx Context) quit() {
+}
+
pub fn (mut ctx Context) draw_rect(x f32, y f32, w f32, h f32, c gx.Color) {
ctx.context.beginPath()
ctx.context.fillStyle = c.to_css_string().str
@@ -423,10 +499,10 @@ fn gg_animation_frame_fn(mut g Context) {
})
}
-fn (mut g Context) handle_mouse_event(event JS.MouseEvent) Event {
+fn (mut g Context) handle_mouse_event(event JS.MouseEvent, typ DOMEventType) Event {
mut e := Event{}
- e.typ = .mouse_down
+ e.typ = typ
e.frame_count = g.frame
match int(event.button) {
@@ -457,3 +533,233 @@ fn (mut g Context) handle_mouse_event(event JS.MouseEvent) Event {
g.mouse_dy = int(event.movementY)
return e
}
+
+fn (mut g Context) handle_keyboard_event(event JS.KeyboardEvent, typ DOMEventType) Event {
+ mut e := Event{}
+ e.typ = typ
+ e.frame_count = g.frame
+
+ match string(event.code) {
+ 'Space' {
+ e.key_code = .space
+ }
+ 'Minus' {
+ e.key_code = .minus
+ }
+ 'Quote' {
+ e.key_code = .apostrophe
+ }
+ 'Comma' {
+ e.key_code = .comma
+ }
+ 'Period' {
+ e.key_code = .period
+ }
+ 'Digit0' {
+ e.key_code = ._0
+ }
+ 'Digit1' {
+ e.key_code = ._1
+ }
+ 'Digit2' {
+ e.key_code = ._2
+ }
+ 'Digit3' {
+ e.key_code = ._3
+ }
+ 'Digit4' {
+ e.key_code = ._4
+ }
+ 'Digit5' {
+ e.key_code = ._5
+ }
+ 'Digit6' {
+ e.key_code = ._6
+ }
+ 'Digit7' {
+ e.key_code = ._7
+ }
+ 'Digit8' {
+ e.key_code = ._8
+ }
+ 'Digit9' {
+ e.key_code = ._9
+ }
+ 'Semicolon' {
+ e.key_code = .semicolon
+ }
+ 'Equal' {
+ e.key_code = .equal
+ }
+ 'KeyA' {
+ e.key_code = .a
+ }
+ 'KeyB' {
+ e.key_code = .b
+ }
+ 'KeyC' {
+ e.key_code = .c
+ }
+ 'KeyD' {
+ e.key_code = .d
+ }
+ 'KeyE' {
+ e.key_code = .e
+ }
+ 'KeyF' {
+ e.key_code = .f
+ }
+ 'KeyG' {
+ e.key_code = .g
+ }
+ 'KeyH' {
+ e.key_code = .h
+ }
+ 'KeyI' {
+ e.key_code = .i
+ }
+ 'KeyJ' {
+ e.key_code = .j
+ }
+ 'KeyK' {
+ e.key_code = .k
+ }
+ 'KeyL' {
+ e.key_code = .l
+ }
+ 'KeyM' {
+ e.key_code = .m
+ }
+ 'KeyN' {
+ e.key_code = .n
+ }
+ 'KeyO' {
+ e.key_code = .o
+ }
+ 'KeyP' {
+ e.key_code = .p
+ }
+ 'KeyQ' {
+ e.key_code = .q
+ }
+ 'KeyR' {
+ e.key_code = .r
+ }
+ 'KeyS' {
+ e.key_code = .s
+ }
+ 'KeyT' {
+ e.key_code = .t
+ }
+ 'KeyU' {
+ e.key_code = .u
+ }
+ 'KeyV' {
+ e.key_code = .v
+ }
+ 'KeyW' {
+ e.key_code = .w
+ }
+ 'KeyX' {
+ e.key_code = .x
+ }
+ 'KeyY' {
+ e.key_code = .y
+ }
+ 'KeyZ' {
+ e.key_code = .z
+ }
+ 'BracketLeft' {
+ e.key_code = .left_bracket
+ }
+ 'BracketRight' {
+ e.key_code = .right_bracket
+ }
+ 'Backslash' {
+ e.key_code = .backslash
+ }
+ 'Backquote' {
+ e.key_code = .grave_accent
+ }
+ 'Escape' {
+ e.key_code = .escape
+ }
+ 'Enter' {
+ e.key_code = .enter
+ }
+ 'Tab' {
+ e.key_code = .tab
+ }
+ 'Backspace' {
+ e.key_code = .backspace
+ }
+ 'Insert' {
+ e.key_code = .insert
+ }
+ 'Delete' {
+ e.key_code = .delete
+ }
+ 'ArrowRight' {
+ e.key_code = .right
+ }
+ 'ArrowLeft' {
+ e.key_code = .left
+ }
+ 'ArrowUp' {
+ e.key_code = .up
+ }
+ 'ArrowDown' {
+ e.key_code = .down
+ }
+ 'PageUp' {
+ e.key_code = .page_up
+ }
+ 'PageDown' {
+ e.key_code = .page_down
+ }
+ 'Home' {
+ e.key_code = .home
+ }
+ 'End' {
+ e.key_code = .end
+ }
+ 'CapsLock' {
+ e.key_code = .caps_lock
+ }
+ 'ScrollLock' {
+ e.key_code = .scroll_lock
+ }
+ 'NumLock' {
+ e.key_code = .num_lock
+ }
+ 'PrintScreen' {
+ e.key_code = .print_screen
+ }
+ 'Pause' {
+ e.key_code = .pause
+ }
+ 'ShiftLeft' {
+ e.key_code = .left_shift
+ }
+ 'ShiftRight' {
+ e.key_code = .right_shift
+ }
+ 'AltLeft' {
+ e.key_code = .left_alt
+ }
+ 'AltRight' {
+ e.key_code = .right_alt
+ }
+ 'ControlLeft' {
+ e.key_code = .left_control
+ }
+ 'ControlRight' {
+ e.key_code = .right_control
+ }
+ else {
+ panic('todo: more keycodes (${string(event.code)})')
+ }
+ }
+
+ return e
+}
diff --git a/vlib/gg/gg.v b/vlib/gg/gg.v
index 3b1f154f2c..b622cfe064 100644
--- a/vlib/gg/gg.v
+++ b/vlib/gg/gg.v
@@ -30,7 +30,7 @@ pub struct PenConfig {
}
pub struct Size {
-pub:
+pub mut:
width int
height int
}
diff --git a/vlib/gg/text_rendering.js.v b/vlib/gg/text_rendering.js.v
new file mode 100644
index 0000000000..8194259f95
--- /dev/null
+++ b/vlib/gg/text_rendering.js.v
@@ -0,0 +1,9 @@
+module gg
+
+import gx
+
+pub fn (mut ctx Context) draw_text(x int, y int, text_ string, cfg gx.TextCfg) {
+ ctx.context.fillStyle = cfg.color.to_css_string().str
+ ctx.context.font = cfg.to_css_string().str
+ ctx.context.fillText(text_.str, x, y)
+}
diff --git a/vlib/gx/color.v b/vlib/gx/color.v
index bdb551ec91..fdcfcbe3dc 100644
--- a/vlib/gx/color.v
+++ b/vlib/gx/color.v
@@ -234,5 +234,5 @@ pub fn color_from_string(s string) Color {
}
pub fn (c Color) to_css_string() string {
- return 'rgb($c.r,$c.g,$c.b)'
+ return 'rgba($c.r,$c.g,$c.b,$c.a)'
}
diff --git a/vlib/gx/text.v b/vlib/gx/text.v
index 986bbc0b48..7ae7a65821 100644
--- a/vlib/gx/text.v
+++ b/vlib/gx/text.v
@@ -19,3 +19,17 @@ pub:
mono bool
italic bool
}
+
+pub fn (cfg TextCfg) to_css_string() string {
+ mut font_style := ''
+ if cfg.bold {
+ font_style += 'bold '
+ }
+ if cfg.mono {
+ font_style += 'mono '
+ }
+ if cfg.italic {
+ font_style += 'italic '
+ }
+ return '$font_style ${cfg.size}px $cfg.family'
+}
diff --git a/vlib/js/dom/dom.js.v b/vlib/js/dom/dom.js.v
index 56f163f424..6db238d1e0 100644
--- a/vlib/js/dom/dom.js.v
+++ b/vlib/js/dom/dom.js.v
@@ -462,6 +462,7 @@ pub interface JS.CanvasRenderingContext2D {
translate(x JS.Number, y JS.Number)
drawFocusIfNeeded(path JS.Path2D, element JS.Element)
stroke()
+ fillText(text JS.String, x JS.Number, y JS.Number)
mut:
lineCap JS.String
lineDashOffset JS.Number
@@ -472,6 +473,7 @@ mut:
strokeStyle FillStyle
globalAlpha JS.Number
globalCompositeOperation JS.String
+ font JS.String
}
pub interface JS.CanvasGradient {
@@ -990,3 +992,16 @@ pub interface JS.ProgressEvent {
target JS.Any
total JS.Number
}
+
+pub interface JS.KeyboardEvent {
+ JS.UIEvent
+ altKey JS.Boolean
+ code JS.String
+ ctrlKey JS.Boolean
+ isComposing JS.Boolean
+ key JS.String
+ location JS.Number
+ metaKey JS.Boolean
+ repeat JS.Boolean
+ shiftKey JS.Boolean
+}
diff --git a/vlib/time/time.js.v b/vlib/time/time.js.v
index f6c552d9ef..40df2356db 100644
--- a/vlib/time/time.js.v
+++ b/vlib/time/time.js.v
@@ -42,3 +42,10 @@ pub fn sleep(dur Duration) {
#let toWait = BigInt(dur.val) / BigInt(time__millisecond)
#while (new Date().getTime() < now + Number(toWait)) {}
}
+
+pub fn ticks() i64 {
+ t := i64(0)
+ #t.val = BigInt(new Date().getTime())
+
+ return t
+}
diff --git a/vlib/time/time_js.js.v b/vlib/time/time_js.js.v
index e2e1b4eac8..f09c474616 100644
--- a/vlib/time/time_js.js.v
+++ b/vlib/time/time_js.js.v
@@ -14,7 +14,7 @@ module time
pub fn sys_mono_now() u64 {
$if js_browser {
mut res := u64(0)
- #res = new u64(window.performance.now() * 1000000)
+ #res = new u64(Math.floor(window.performance.now() * 1000000))
return res
} $else $if js_node {
diff --git a/vlib/v/gen/js/js.v b/vlib/v/gen/js/js.v
index dc830c4e7c..4605dd22e1 100644
--- a/vlib/v/gen/js/js.v
+++ b/vlib/v/gen/js/js.v
@@ -3437,7 +3437,7 @@ fn (mut g JsGen) gen_type_cast_expr(it ast.CastExpr) {
}
if (from_type_sym.name == 'Any' && from_type_sym.language == .js)
- || from_type_sym.name == 'JS.Any' {
+ || from_type_sym.name == 'JS.Any' || from_type_sym.name == 'voidptr' {
if it.typ.is_ptr() {
g.write('new \$ref(')
}