gg: add text rendering, keyboard event handling for JS and other fixes (#12932)

pull/12938/head
playX 2021-12-22 13:26:52 +03:00 committed by GitHub
parent 6eb44f472a
commit 2b9f993574
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 596 additions and 25 deletions

View File

@ -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.

View File

@ -0,0 +1,6 @@
<body class="main">
<title>gg</title>
<canvas style="border: 1px solid black;" width="700" height="800" id="canvas"></canvas>
<script type="text/javascript" src="snek.js"></script>
</body>

View File

@ -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()
}

View File

@ -36,7 +36,7 @@ pub struct Event {
pub mut: pub mut:
frame_count u64 frame_count u64
typ DOMEventType typ DOMEventType
key_code DOMKeyCode key_code KeyCode
char_code u32 char_code u32
key_repeat bool key_repeat bool
modifiers u32 modifiers u32
@ -252,7 +252,13 @@ pub:
enable_dragndrop bool // enable file dropping (drag'n'drop), default is false 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_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) 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 { pub struct Context {
@ -284,9 +290,33 @@ pub mut:
pressed_keys [key_code_max]bool // an array representing all currently pressed keys 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, pressed_keys_edge [key_code_max]bool // true when the previous state of pressed_keys,
context JS.CanvasRenderingContext2D [noinit] context JS.CanvasRenderingContext2D [noinit]
canvas JS.HTMLCanvasElement [noinit]
// *before* the current event was different // *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 { pub fn new_context(cfg Config) &Context {
mut g := &Context{} mut g := &Context{}
@ -294,24 +324,30 @@ pub fn new_context(cfg Config) &Context {
g.width = cfg.width g.width = cfg.width
g.height = cfg.height g.height = cfg.height
g.ui_mode = cfg.ui_mode g.ui_mode = cfg.ui_mode
mut sz := gg.size
sz.height = g.height
sz.width = g.width
g.config = cfg g.config = cfg
if isnil(cfg.user_data) { if isnil(cfg.user_data) {
g.user_data = g g.user_data = g
} }
g.window = dom.window() g.window = dom.window()
ctx := cfg.canvas.getContext('2d'.str, js_undefined()) or { panic('') } document := dom.document
match ctx { canvas_elem := document.getElementById(cfg.canvas.str) or {
JS.CanvasRenderingContext2D { panic('gg: cannot get canvas element')
g.context = ctx
}
else {
panic('gg: cannot get 2D context')
}
} }
canvas := get_canvas(canvas_elem)
g.canvas = canvas
g.context = get_context(g.canvas)
mouse_down_event_handler := fn [mut g] (event JS.Event) { mouse_down_event_handler := fn [mut g] (event JS.Event) {
match event { match event {
JS.MouseEvent { 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) { if !isnil(g.config.click_fn) {
f := g.config.click_fn f := g.config.click_fn
f(e.mouse_x, e.mouse_y, e.mouse_button, g.config.user_data) 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) { mouse_up_event_handler := fn [mut g] (event JS.Event) {
match event { match event {
JS.MouseEvent { 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) { if !isnil(g.config.unclick_fn) {
f := g.config.unclick_fn f := g.config.unclick_fn
f(e.mouse_x, e.mouse_y, e.mouse_button, g.config.user_data) 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) { mouse_move_event_handler := fn [mut g] (event JS.Event) {
match event { match event {
JS.MouseEvent { 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) { if !isnil(g.config.move_fn) {
f := g.config.move_fn f := g.config.move_fn
f(e.mouse_x, e.mouse_y, g.config.user_data) 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) { mouse_leave_event_handler := fn [mut g] (event JS.Event) {
match event { match event {
JS.MouseEvent { 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) { if !isnil(g.config.leave_fn) {
f := g.config.leave_fn f := g.config.leave_fn
f(e, g.config.user_data) 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) { mouse_enter_event_handler := fn [mut g] (event JS.Event) {
match event { match event {
JS.MouseEvent { 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) { if !isnil(g.config.enter_fn) {
f := g.config.enter_fn f := g.config.enter_fn
f(e, g.config.user_data) f(e, g.config.user_data)
@ -371,11 +423,32 @@ pub fn new_context(cfg Config) &Context {
else {} 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{}) dom.window().addEventListener('mouseup'.str, mouse_up_event_handler, JS.EventListenerOptions{})
cfg.canvas.addEventListener('mousemove'.str, mouse_move_event_handler, JS.EventListenerOptions{}) g.canvas.addEventListener('mousemove'.str, mouse_move_event_handler, JS.EventListenerOptions{})
cfg.canvas.addEventListener('mouseleave'.str, mouse_leave_event_handler, JS.EventListenerOptions{}) g.canvas.addEventListener('mouseleave'.str, mouse_leave_event_handler, JS.EventListenerOptions{})
cfg.canvas.addEventListener('mouseenter'.str, mouse_enter_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 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() 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) { pub fn (mut ctx Context) draw_rect(x f32, y f32, w f32, h f32, c gx.Color) {
ctx.context.beginPath() ctx.context.beginPath()
ctx.context.fillStyle = c.to_css_string().str 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{} mut e := Event{}
e.typ = .mouse_down e.typ = typ
e.frame_count = g.frame e.frame_count = g.frame
match int(event.button) { 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) g.mouse_dy = int(event.movementY)
return e 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
}

View File

@ -30,7 +30,7 @@ pub struct PenConfig {
} }
pub struct Size { pub struct Size {
pub: pub mut:
width int width int
height int height int
} }

View File

@ -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)
}

View File

@ -234,5 +234,5 @@ pub fn color_from_string(s string) Color {
} }
pub fn (c Color) to_css_string() string { 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)'
} }

View File

@ -19,3 +19,17 @@ pub:
mono bool mono bool
italic 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'
}

View File

@ -462,6 +462,7 @@ pub interface JS.CanvasRenderingContext2D {
translate(x JS.Number, y JS.Number) translate(x JS.Number, y JS.Number)
drawFocusIfNeeded(path JS.Path2D, element JS.Element) drawFocusIfNeeded(path JS.Path2D, element JS.Element)
stroke() stroke()
fillText(text JS.String, x JS.Number, y JS.Number)
mut: mut:
lineCap JS.String lineCap JS.String
lineDashOffset JS.Number lineDashOffset JS.Number
@ -472,6 +473,7 @@ mut:
strokeStyle FillStyle strokeStyle FillStyle
globalAlpha JS.Number globalAlpha JS.Number
globalCompositeOperation JS.String globalCompositeOperation JS.String
font JS.String
} }
pub interface JS.CanvasGradient { pub interface JS.CanvasGradient {
@ -990,3 +992,16 @@ pub interface JS.ProgressEvent {
target JS.Any target JS.Any
total JS.Number 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
}

View File

@ -42,3 +42,10 @@ pub fn sleep(dur Duration) {
#let toWait = BigInt(dur.val) / BigInt(time__millisecond) #let toWait = BigInt(dur.val) / BigInt(time__millisecond)
#while (new Date().getTime() < now + Number(toWait)) {} #while (new Date().getTime() < now + Number(toWait)) {}
} }
pub fn ticks() i64 {
t := i64(0)
#t.val = BigInt(new Date().getTime())
return t
}

View File

@ -14,7 +14,7 @@ module time
pub fn sys_mono_now() u64 { pub fn sys_mono_now() u64 {
$if js_browser { $if js_browser {
mut res := u64(0) mut res := u64(0)
#res = new u64(window.performance.now() * 1000000) #res = new u64(Math.floor(window.performance.now() * 1000000))
return res return res
} $else $if js_node { } $else $if js_node {

View File

@ -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) 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() { if it.typ.is_ptr() {
g.write('new \$ref(') g.write('new \$ref(')
} }