From 2b9f9935745e238ae8bd1d1ee59a11d37bda65ff Mon Sep 17 00:00:00 2001 From: playX Date: Wed, 22 Dec 2021 13:26:52 +0300 Subject: [PATCH] gg: add text rendering, keyboard event handling for JS and other fixes (#12932) --- examples/snek/README.md | 17 ++ examples/snek/index.html | 6 + examples/snek/snek.js.v | 197 ++++++++++++++++++++ vlib/gg/gg.js.v | 348 +++++++++++++++++++++++++++++++++--- vlib/gg/gg.v | 2 +- vlib/gg/text_rendering.js.v | 9 + vlib/gx/color.v | 2 +- vlib/gx/text.v | 14 ++ vlib/js/dom/dom.js.v | 15 ++ vlib/time/time.js.v | 7 + vlib/time/time_js.js.v | 2 +- vlib/v/gen/js/js.v | 2 +- 12 files changed, 596 insertions(+), 25 deletions(-) create mode 100644 examples/snek/README.md create mode 100644 examples/snek/index.html create mode 100644 examples/snek/snek.js.v create mode 100644 vlib/gg/text_rendering.js.v 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(') }