diff --git a/examples/2048/2048.v b/examples/2048/2048.v index da2df3ad71..7c0bed03fa 100644 --- a/examples/2048/2048.v +++ b/examples/2048/2048.v @@ -1,10 +1,22 @@ import gg import gx import math +import math.easing import os.asset import rand import time +const zooming_percent_per_frame = 5 +const movement_percent_per_frame = 10 + +const window_title = 'V 2048' +const default_window_width = 544 +const default_window_height = 560 + +const possible_moves = [Direction.up, .right, .down, .left] +const predictions_per_move = 300 +const prediction_depth = 8 + struct App { mut: gg &gg.Context = unsafe { nil } @@ -14,7 +26,8 @@ mut: theme_idx int board Board undo []Undo - atickers [4][4]int + atickers [4][4]f64 + mtickers [4][4]f64 state GameState = .play tile_format TileFormat = .normal moves int @@ -111,14 +124,6 @@ const themes = [ ] }, ] -const window_title = 'V 2048' -const default_window_width = 544 -const default_window_height = 560 -const animation_length = 10 // frames - -const possible_moves = [Direction.up, .right, .down, .left] -const predictions_per_move = 300 -const prediction_depth = 8 struct Pos { x int = -1 @@ -128,6 +133,7 @@ struct Pos { struct Board { mut: field [4][4]int + oidxs [4][4]u32 // old indexes of the fields, when != 0; each index is an encoding of its y,x coordinates = y << 16 | x points int shifts int } @@ -141,6 +147,7 @@ struct TileLine { ypos int mut: field [5]int + oidxs [5]u32 points int shifts int } @@ -201,6 +208,7 @@ fn (b Board) transpose() Board { for y in 0 .. 4 { for x in 0 .. 4 { res.field[y][x] = b.field[x][y] + res.oidxs[y][x] = b.oidxs[x][y] } } return res @@ -211,6 +219,7 @@ fn (b Board) hmirror() Board { for y in 0 .. 4 { for x in 0 .. 4 { res.field[y][x] = b.field[y][3 - x] + res.oidxs[y][x] = b.oidxs[y][3 - x] } } return res @@ -241,6 +250,7 @@ fn (t TileLine) to_left() TileLine { res.shifts++ for k := x; k < right_border_idx; k++ { res.field[k] = res.field[k + 1] + res.oidxs[k] = res.oidxs[k + 1] } remaining_zeros-- } @@ -255,6 +265,7 @@ fn (t TileLine) to_left() TileLine { if res.field[x] == res.field[x + 1] { for k := x; k < right_border_idx; k++ { res.field[k] = res.field[k + 1] + res.oidxs[k] = res.oidxs[k + 1] } res.shifts++ res.field[x]++ @@ -272,18 +283,29 @@ fn (b Board) to_left() Board { } for x in 0 .. 4 { hline.field[x] = b.field[y][x] + hline.oidxs[x] = b.oidxs[y][x] } reshline := hline.to_left() res.shifts += reshline.shifts res.points += reshline.points for x in 0 .. 4 { res.field[y][x] = reshline.field[x] + res.oidxs[y][x] = reshline.oidxs[x] } } return res } -fn (b Board) move(d Direction) (Board, bool) { +fn yx2i(y int, x int) u32 { + return u32(y) << 16 | u32(x) +} + +fn (mut b Board) move(d Direction) (Board, bool) { + for y in 0 .. 4 { + for x in 0 .. 4 { + b.oidxs[y][x] = yx2i(y, x) + } + } new := match d { .left { b.to_left() } .right { b.hmirror().to_left().hmirror() } @@ -291,9 +313,9 @@ fn (b Board) move(d Direction) (Board, bool) { .down { b.transpose().hmirror().to_left().hmirror().transpose() } } // If the board hasn't changed, it's an illegal move, don't allow it. - for x in 0 .. 4 { - for y in 0 .. 4 { - if b.field[x][y] != new.field[x][y] { + for y in 0 .. 4 { + for x in 0 .. 4 { + if b.field[y][x] != new.field[y][x] { return new, true } } @@ -324,11 +346,10 @@ fn (mut b Board) is_game_over() bool { fn (mut app App) update_tickers() { for y in 0 .. 4 { for x in 0 .. 4 { - mut old := app.atickers[y][x] - if old > 0 { - old-- - app.atickers[y][x] = old - } + app.atickers[y][x] = math.clip(app.atickers[y][x] - f64(zooming_percent_per_frame) / 100.0, + 0.0, 1.0) + app.mtickers[y][x] = math.clip(app.mtickers[y][x] - f64(movement_percent_per_frame) / 100.0, + 0.0, 1.0) } } } @@ -339,6 +360,7 @@ fn (mut app App) new_game() { for x in 0 .. 4 { app.board.field[y][x] = 0 app.atickers[y][x] = 0 + app.mtickers[y][x] = 0 } } app.state = .play @@ -387,23 +409,26 @@ fn (mut b Board) place_random_tile() (Pos, int) { value := rand.f64n(1.0) or { 0.0 } random_value := if value < 0.9 { 1 } else { 2 } b.field[empty_pos.y][empty_pos.x] = random_value + b.oidxs[empty_pos.y][empty_pos.x] = yx2i(empty_pos.y, empty_pos.x) return empty_pos, random_value } return Pos{}, 0 } fn (mut app App) new_random_tile() { + // do not animate empty fields: for y in 0 .. 4 { for x in 0 .. 4 { fidx := app.board.field[y][x] if fidx == 0 { app.atickers[y][x] = 0 + app.board.oidxs[y][x] = 0xFFFF_FFFF } } } empty_pos, random_value := app.board.place_random_tile() if random_value > 0 { - app.atickers[empty_pos.y][empty_pos.x] = animation_length + app.atickers[empty_pos.y][empty_pos.x] = 1.0 } if app.state != .freeplay { app.check_for_victory() @@ -414,6 +439,13 @@ fn (mut app App) new_random_tile() { fn (mut app App) apply_new_board(new Board) { old := app.board app.moves++ + for y in 0 .. 4 { + for x in 0 .. 4 { + if old.oidxs[y][x] != new.oidxs[y][x] { + app.mtickers[y][x] = 1.0 + } + } + } app.board = new app.undo << Undo{old, app.state} app.new_random_tile() @@ -623,62 +655,114 @@ fn (app &App) draw_tiles() { // Draw the padding around the tiles app.gg.draw_rounded_rect_filled(xstart, ystart, tiles_size, tiles_size, tiles_size / 24, app.theme.padding_color) - // Draw the actual tiles + + // Draw empty tiles: + for y in 0 .. 4 { + for x in 0 .. 4 { + tw := app.ui.tile_size + th := tw // square tiles, w == h + xoffset := xstart + app.ui.padding_size + x * toffset + yoffset := ystart + app.ui.padding_size + y * toffset + app.gg.draw_rounded_rect_filled(xoffset, yoffset, tw, th, tw / 8, app.theme.tile_colors[0]) + } + } + + // Draw the already placed and potentially moving tiles: for y in 0 .. 4 { for x in 0 .. 4 { tidx := app.board.field[y][x] - tile_color := if tidx < app.theme.tile_colors.len { - app.theme.tile_colors[tidx] - } else { - // If there isn't a specific color for this tile, reuse the last color available - app.theme.tile_colors.last() + oidx := app.board.oidxs[y][x] + if tidx == 0 || oidx == 0xFFFF_FFFF { + continue } - anim_size := animation_length - app.atickers[y][x] - tw := int(f64(app.ui.tile_size) / animation_length * anim_size) - th := tw // square tiles, w == h - xoffset := xstart + app.ui.padding_size + x * toffset + (app.ui.tile_size - tw) / 2 - yoffset := ystart + app.ui.padding_size + y * toffset + (app.ui.tile_size - th) / 2 - app.gg.draw_rounded_rect_filled(xoffset, yoffset, tw, th, tw / 8, tile_color) - if tidx != 0 { // 0 == blank spot - xpos := xoffset + tw / 2 - ypos := yoffset + th / 2 - mut fmt := app.label_format(.tile) - fmt = gx.TextCfg{ - ...fmt - size: int(f32(fmt.size - 1) / animation_length * anim_size) - } - match app.tile_format { - .normal { - app.gg.draw_text(xpos, ypos, '${1 << tidx}', fmt) - } - .log { - app.gg.draw_text(xpos, ypos, '${tidx}', fmt) - } - .exponent { - app.gg.draw_text(xpos, ypos, '2', fmt) - fs2 := int(f32(fmt.size) * 0.67) - app.gg.draw_text(xpos + app.ui.tile_size / 10, ypos - app.ui.tile_size / 8, - '${tidx}', gx.TextCfg{ - ...fmt - size: fs2 - align: gx.HorizontalAlign.left - }) - } - .shifts { - fs2 := int(f32(fmt.size) * 0.6) - app.gg.draw_text(xpos, ypos, '2<<${tidx - 1}', gx.TextCfg{ - ...fmt - size: fs2 - }) - } - .none {} // Don't draw any text here, colors only - .end {} // Should never get here - } + app.draw_one_tile(x, y, tidx) + } + } + + // Draw the newly placed random tiles on top of everything else: + for y in 0 .. 4 { + for x in 0 .. 4 { + tidx := app.board.field[y][x] + oidx := app.board.oidxs[y][x] + if oidx == 0xFFFF_FFFF && tidx != 0 { + app.draw_one_tile(x, y, tidx) } } } } +fn (app &App) draw_one_tile(x int, y int, tidx int) { + xstart := app.ui.x_padding + app.ui.border_size + ystart := app.ui.y_padding + app.ui.border_size + app.ui.header_size + toffset := app.ui.tile_size + app.ui.padding_size + oidx := app.board.oidxs[y][x] + oy := oidx >> 16 + ox := oidx & 0xFFFF + mut dx := 0 + mut dy := 0 + if oidx != 0xFFFF_FFFF { + scaling := app.ui.tile_size * easing.in_out_quint(app.mtickers[y][x]) + if ox != x { + dx = math.clip(int(scaling * (f64(ox) - f64(x))), -4 * app.ui.tile_size, 4 * app.ui.tile_size) + } + if oy != y { + dy = math.clip(int(scaling * (f64(oy) - f64(y))), -4 * app.ui.tile_size, 4 * app.ui.tile_size) + } + } + tile_color := if tidx < app.theme.tile_colors.len { + app.theme.tile_colors[tidx] + } else { + // If there isn't a specific color for this tile, reuse the last color available + app.theme.tile_colors.last() + } + anim_size := 1.0 - app.atickers[y][x] + tw := int(f64(anim_size * app.ui.tile_size)) + th := tw // square tiles, w == h + xoffset := dx + xstart + app.ui.padding_size + x * toffset + (app.ui.tile_size - tw) / 2 + yoffset := dy + ystart + app.ui.padding_size + y * toffset + (app.ui.tile_size - th) / 2 + app.gg.draw_rounded_rect_filled(xoffset, yoffset, tw, th, tw / 8, tile_color) + if tidx != 0 { // 0 == blank spot + xpos := xoffset + tw / 2 + ypos := yoffset + th / 2 + mut fmt := app.label_format(.tile) + fmt = gx.TextCfg{ + ...fmt + size: int(anim_size * (fmt.size - 1)) + } + match app.tile_format { + .normal { + app.gg.draw_text(xpos, ypos, '${1 << tidx}', fmt) + } + .log { + app.gg.draw_text(xpos, ypos, '${tidx}', fmt) + } + .exponent { + app.gg.draw_text(xpos, ypos, '2', fmt) + fs2 := int(f32(fmt.size) * 0.67) + app.gg.draw_text(xpos + app.ui.tile_size / 10, ypos - app.ui.tile_size / 8, + '${tidx}', gx.TextCfg{ + ...fmt + size: fs2 + align: gx.HorizontalAlign.left + }) + } + .shifts { + fs2 := int(f32(fmt.size) * 0.6) + app.gg.draw_text(xpos, ypos, '2<<${tidx - 1}', gx.TextCfg{ + ...fmt + size: fs2 + }) + } + .none {} // Don't draw any text here, colors only + .end {} // Should never get here + } + // oidx_fmt := gx.TextCfg{...fmt,size: 14} + // app.gg.draw_text(xoffset + 50, yoffset + 15, 'y:${oidx >> 16}|x:${oidx & 0xFFFF}|m:${app.mtickers[y][x]:5.3f}', oidx_fmt) + // app.gg.draw_text(xoffset + 52, yoffset + 30, 'ox:${ox}|oy:${oy}', oidx_fmt) + // app.gg.draw_text(xoffset + 52, yoffset + 85, 'dx:${dx}|dy:${dy}', oidx_fmt) + } +} + fn (mut app App) handle_touches() { s, e := app.touch.start, app.touch.end adx, ady := math.abs(e.pos.x - s.pos.x), math.abs(e.pos.y - s.pos.y) @@ -733,7 +817,7 @@ fn (mut app App) handle_swipe() { adx, ady := math.abs(dx), math.abs(dy) dmin := if math.min(adx, ady) > 0 { math.min(adx, ady) } else { 1 } dmax := if math.max(adx, ady) > 0 { math.max(adx, ady) } else { 1 } - tdiff := int(e.time.unix_milli() - s.time.unix_milli()) + tdiff := (e.time - s.time).milliseconds() // TODO: make this calculation more accurate (don't use arbitrary numbers) min_swipe_distance := int(math.sqrt(math.min(w, h) * tdiff / 100)) + 20 if dmax < min_swipe_distance { diff --git a/vlib/builtin/float.c.v b/vlib/builtin/float.c.v index 018b0293aa..1e1d48488a 100644 --- a/vlib/builtin/float.c.v +++ b/vlib/builtin/float.c.v @@ -135,10 +135,6 @@ pub fn (x f32) strlong() string { return strconv.f32_to_str_l(x) } -/* ------------------------ ------ C functions ----- -*/ // f32_abs returns the absolute value of `a` as a `f32` value. // Example: assert f32_abs(-2.0) == 2.0 @[inline] @@ -149,38 +145,38 @@ pub fn f32_abs(a f32) f32 { // f64_abs returns the absolute value of `a` as a `f64` value. // Example: assert f64_abs(-2.0) == f64(2.0) @[inline] -fn f64_abs(a f64) f64 { +pub fn f64_abs(a f64) f64 { return if a < 0 { -a } else { a } } -// f32_max returns the largest `f32` of input `a` and `b`. -// Example: assert f32_max(2.0,3.0) == 3.0 -@[inline] -pub fn f32_max(a f32, b f32) f32 { - return if a > b { a } else { b } -} - -// f32_min returns the smallest `f32` of input `a` and `b`. +// f32_min returns the smaller `f32` of input `a` and `b`. // Example: assert f32_min(2.0,3.0) == 2.0 @[inline] pub fn f32_min(a f32, b f32) f32 { return if a < b { a } else { b } } -// f64_max returns the largest `f64` of input `a` and `b`. +// f32_max returns the larger `f32` of input `a` and `b`. +// Example: assert f32_max(2.0,3.0) == 3.0 +@[inline] +pub fn f32_max(a f32, b f32) f32 { + return if a > b { a } else { b } +} + +// f64_min returns the smaller `f64` of input `a` and `b`. +// Example: assert f64_min(2.0,3.0) == 2.0 +@[inline] +pub fn f64_min(a f64, b f64) f64 { + return if a < b { a } else { b } +} + +// f64_max returns the larger `f64` of input `a` and `b`. // Example: assert f64_max(2.0,3.0) == 3.0 @[inline] pub fn f64_max(a f64, b f64) f64 { return if a > b { a } else { b } } -// f64_min returns the smallest `f64` of input `a` and `b`. -// Example: assert f64_min(2.0,3.0) == 2.0 -@[inline] -fn f64_min(a f64, b f64) f64 { - return if a < b { a } else { b } -} - // eq_epsilon returns true if the `f32` is equal to input `b`. // using an epsilon of typically 1E-5 or higher (backend/compiler dependent). // Example: assert f32(2.0).eq_epsilon(2.0) diff --git a/vlib/math/interpolation.v b/vlib/math/interpolation.v new file mode 100644 index 0000000000..d2ce4f56cc --- /dev/null +++ b/vlib/math/interpolation.v @@ -0,0 +1,26 @@ +module math + +// mix performs a linear interpolation (LERP) mix between `start` and `end`, using `t` to weight between them. +// `t` should be in the closed interval [0, 1]. +// For `t` == 0, the output is `x`. +// Note: mix is calculated in such a way, that the output *will* be `y`, when `t` == 1.0 . +// See: https://registry.khronos.org/OpenGL-Refpages/gl4/html/mix.xhtml +// Also: https://en.wikipedia.org/wiki/Linear_interpolation . +@[inline] +pub fn mix[T](start T, end T, t T) T { + return start * (1 - t) + end * t +} + +// clip constrain the given value `x`, to lie between two further values `min_value` and `max_value`. +// See: https://registry.khronos.org/OpenGL-Refpages/gl4/html/clamp.xhtml +// Also: https://en.wikipedia.org/wiki/Clamp_(function) +@[inline] +pub fn clip[T](x T, min_value T, max_value T) T { + return if x > max_value { + max_value + } else if x < min_value { + min_value + } else { + x + } +} diff --git a/vlib/math/interpolation_bezier.v b/vlib/math/interpolation_bezier.v new file mode 100644 index 0000000000..56a285c0c0 --- /dev/null +++ b/vlib/math/interpolation_bezier.v @@ -0,0 +1,69 @@ +module math + +// BezierPoint represents point coordinates as floating point numbers. +// This type is used as the output of the cubic_bezier family of functions. +pub struct BezierPoint { +pub mut: + x f64 + y f64 +} + +// cubic_bezier returns a linear interpolation between the control points, +// specified by their X and Y coordinates in a single array of points `p`, and given the parameter t, +// varying between 0.0 and 1.0 . +// When `t` == 0.0, the output is P[0] . +// When `t` == 1.0, the output is P[3] . +// The points x[1],y[1] and x[2],y[2], serve as attractors. +@[direct_array_access; inline] +pub fn cubic_bezier(t f64, p []BezierPoint) BezierPoint { + if p.len != 4 { + panic('invalid p.len') + } + return cubic_bezier_coords(t, p[0].x, p[1].x, p[2].x, p[3].x, p[0].y, p[1].y, p[2].y, + p[3].y) +} + +// cubic_bezier_a returns a linear interpolation between the control points, +// specified by their X and Y coordinates in 2 arrays, and given the parameter t, +// varying between 0.0 and 1.0 . +// When `t` == 0.0, the output is x[0],y[0] . +// When `t` == 1.0, the output is x[3],y[3] . +// The points x[1],y[1] and x[2],y[2], serve as attractors. +@[direct_array_access; inline] +pub fn cubic_bezier_a(t f64, x []f64, y []f64) BezierPoint { + if x.len != 4 { + panic('invalid x.len') + } + if y.len != 4 { + panic('invalid y.len') + } + return cubic_bezier_coords(t, x[0], x[1], x[2], x[3], y[0], y[1], y[2], y[3]) +} + +// cubic_bezier_fa returns a linear interpolation between the control points, +// specified by their X and Y coordinates in 2 fixed arrays, and given the parameter t, +// varying between 0.0 and 1.0 . +// When `t` == 0.0, the output is x[0],y[0] . +// When `t` == 1.0, the output is x[3],y[3] . +// The points x[1],y[1] and x[2],y[2], serve as attractors. +@[direct_array_access; inline] +pub fn cubic_bezier_fa(t f64, x [4]f64, y [4]f64) BezierPoint { + return cubic_bezier_coords(t, x[0], x[1], x[2], x[3], y[0], y[1], y[2], y[3]) +} + +// cubic_bezier_coords returns a linear interpolation between the control points, +// specified by their X and Y coordinates, and given the parameter t, +// varying between 0.0 and 1.0 . +// When `t` == 0.0, the output is x0,y0 . +// When `t` == 1.0, the output is x3,y3 . +// The points x1,y1 and x2,y2, serve as attractors. +@[inline] +pub fn cubic_bezier_coords(t f64, x0 f64, x1 f64, x2 f64, x3 f64, y0 f64, y1 f64, y2 f64, y3 f64) BezierPoint { + p0 := pow(1 - t, 3) + p1 := 3 * t * pow(1 - t, 2) + p2 := 3 * (1 - t) * pow(t, 2) + p3 := pow(t, 3) + xt := p0 * x0 + p1 * x1 + p2 * x2 + p3 * x3 + yt := p0 * y0 + p1 * y1 + p2 * y2 + p3 * y3 + return BezierPoint{xt, yt} +} diff --git a/vlib/math/interpolation_test.v b/vlib/math/interpolation_test.v new file mode 100644 index 0000000000..1b73fe15a5 --- /dev/null +++ b/vlib/math/interpolation_test.v @@ -0,0 +1,67 @@ +import math + +fn test_mix() { + assert math.mix(0.0, 100.0, 0.0) == 0.0 + assert math.mix(0.0, 100.0, 0.1) == 10.0 + assert math.mix(0.0, 100.0, 0.2) == 20.0 + assert math.mix(0.0, 100.0, 0.5) == 50.0 + assert math.mix(0.0, 100.0, 0.8) == 80.0 + assert math.mix(0.0, 100.0, 0.9) == 90.0 + assert math.mix(0.0, 100.0, 1.0) == 100.0 + + assert math.mix(100.0, 500.0, 0.0) == 100.0 + assert math.mix(100.0, 500.0, 0.1) == 140.0 + assert math.mix(100.0, 500.0, 0.2) == 180.0 + assert math.mix(100.0, 500.0, 0.5) == 300.0 + assert math.mix(100.0, 500.0, 0.8) == 420.0 + assert math.mix(100.0, 500.0, 0.9) == 460.0 + assert math.mix(100.0, 500.0, 1.0) == 500.0 +} + +fn test_clip() { + assert math.clip(0.0, 10.0, 50.0) == 10.0 + assert math.clip(5.5, 10.0, 50.0) == 10.0 + assert math.clip(10.0, 10.0, 50.0) == 10.0 + assert math.clip(20.0, 10.0, 50.0) == 20.0 + assert math.clip(50.0, 10.0, 50.0) == 50.0 + assert math.clip(80.0, 10.0, 50.0) == 50.0 + assert math.clip(90.5, 10.0, 50.0) == 50.0 + + assert math.clip(0, 10, 50) == 10 + assert math.clip(5, 10, 50) == 10 + assert math.clip(10, 10, 50) == 10 + assert math.clip(20, 10, 50) == 20 + assert math.clip(50, 10, 50) == 50 + assert math.clip(80, 10, 50) == 50 + assert math.clip(90, 10, 50) == 50 +} + +// The test curve control points are taken from: https://cubic-bezier.com/#.19,-0.09,.42,1.19 +const b = [ + math.BezierPoint{0, 0}, + math.BezierPoint{0.19, -0.09}, + math.BezierPoint{0.42, 1.19}, + math.BezierPoint{1.0, 1.0}, +] +const bx = b.map(it.x) +const by = b.map(it.y) +const bx_fa = [4]f64{init: b[index].x} +const by_fa = [4]f64{init: b[index].y} + +fn test_cubic_bezier() { + assert math.cubic_bezier(0.0, b) == math.BezierPoint{b[0].x, b[0].x} + assert math.cubic_bezier(0.5, b) == math.BezierPoint{0.35375, 0.5375} + assert math.cubic_bezier(1.0, b) == math.BezierPoint{b[3].x, b[3].x} +} + +fn test_cubic_bezier_a() { + assert math.cubic_bezier_a(0.0, bx, by) == math.BezierPoint{bx[0], by[0]} + assert math.cubic_bezier_a(0.5, bx, by) == math.BezierPoint{0.35375, 0.5375} + assert math.cubic_bezier_a(1.0, bx, by) == math.BezierPoint{bx[3], by[3]} +} + +fn test_cubic_bezier_fa() { + assert math.cubic_bezier_fa(0.0, bx_fa, by_fa) == math.BezierPoint{bx_fa[0], by_fa[0]} + assert math.cubic_bezier_fa(0.5, bx_fa, by_fa) == math.BezierPoint{0.35375, 0.5375} + assert math.cubic_bezier_fa(1.0, bx_fa, by_fa) == math.BezierPoint{bx_fa[3], by_fa[3]} +}