From 29818b1b0b0374491bb19182e222c81caeaba3a9 Mon Sep 17 00:00:00 2001 From: John Alanbrook Date: Wed, 25 Feb 2026 16:58:06 -0600 Subject: [PATCH] fix examples --- camera.cm | 86 +++++++++++-------- compositor.cm | 5 +- examples/bunnymark/main.ce | 8 +- examples/chess/chess.ce | 152 +++++++++++++++++++++++++-------- examples/chess/grid.cm | 22 +++-- examples/chess/movement.cm | 9 +- examples/chess/pieces.cm | 8 +- examples/chess/rules.cm | 6 +- examples/pong/main.ce | 4 +- examples/snake/main.ce | 13 ++- examples/tetris/main.ce | 29 ++++--- imgui.cpp | 2 +- input.cm | 2 +- sdl_gpu.cm | 4 +- shaders/msl/text_msdf.frag.msl | 64 ++++++++++++++ shaders/msl/text_sdf.frag.msl | 59 +++++++++++++ 16 files changed, 354 insertions(+), 119 deletions(-) create mode 100644 shaders/msl/text_msdf.frag.msl create mode 100644 shaders/msl/text_sdf.frag.msl diff --git a/camera.cm b/camera.cm index 2eab6490..2ef15525 100644 --- a/camera.cm +++ b/camera.cm @@ -1,7 +1,9 @@ +var backend = use('sdl_gpu') + var cam = {} -/* - presentation can be one of +/* + presentation can be one of letterbox overscan stretch @@ -13,6 +15,27 @@ function rect_contains_pt(rect, x, y) { y >= rect.y && y <= rect.y + rect.height } +function place_rect(src, dst, mode) { + if (mode == 'stretch') + return {x: 0, y: 0, width: dst.width, height: dst.height} + + var sx = 0, sy = 0, s = 0, w = 0, h = 0, scale = 0 + if (mode == 'integer_scale') { + sx = floor(dst.width / src.width) + sy = floor(dst.height / src.height) + s = max(1, min(sx, sy)) + w = src.width * s + h = src.height * s + return {x: (dst.width - w) / 2, y: (dst.height - h) / 2, width: w, height: h} + } + + // letterbox (default) + scale = min(dst.width / src.width, dst.height / src.height) + w = src.width * scale + h = src.height * scale + return {x: (dst.width - w) / 2, y: (dst.height - h) / 2, width: w, height: h} +} + var basecam = { pos: {x:0,y:0}, ortho:true, @@ -30,36 +53,32 @@ var basecam = { return this.y/this.x; }, - screen_rect() { - var src = { width: this.width, height: this.height } - var dst = { x: 0, y: 0, width: gameres.width, height: gameres.height } - return place_rect(src, dst, this.presentation, this.align_x, this.align_y, false) - }, - rect() { return {x:0, y:0, width: this.width, height: this.height} }, - + sensor() { + var ws = backend.get_window_size() return { - width: gameres.width, - height: gameres.height, + width: ws.width, + height: ws.height, } }, - // The rectangle (in window pixels) where this camera’s target will be drawn + // The rectangle (in window pixels) where this camera’s target will be drawn screen_rect() { var src = { width: this.width, height: this.height } - var dst = { x: 0, y: 0, width: gameres.width, height: gameres.height } - return place_rect(src, dst, this.presentation, this.align_x, this.align_y, false) + var ws = backend.get_window_size() + var dst = { x: 0, y: 0, width: ws.width, height: ws.height } + return place_rect(src, dst, this.presentation) }, // --- Space converters --------------------------------------------------- // World -> View UV [0..1] independent of pixels/letterbox world_to_view_uv(wx, wy) { - var ax = this.anchor[0], ay = this.anchor[1] - var u = (wx - this.pos[0]) / this.width + ax - var v = (wy - this.pos[1]) / this.height + ay + var ax = this.anchor.x, ay = this.anchor.y + var u = (wx - this.pos.x) / this.width + ax + var v = (wy - this.pos.y) / this.height + ay // apply viewport (maps camera-local UV into sub-rect) var vp = this.viewport @@ -73,9 +92,9 @@ var basecam = { var vp = this.viewport var uu = (u - vp.x) / vp.width var vv = (v - vp.y) / vp.height - var ax = this.anchor[0], ay = this.anchor[1] - var wx = this.pos[0] + (uu - ax) * this.width - var wy = this.pos[1] + (vv - ay) * this.height + var ax = this.anchor.x, ay = this.anchor.y + var wx = this.pos.x + (uu - ax) * this.width + var wy = this.pos.y + (vv - ay) * this.height return { x: wx, y: wy } }, @@ -91,39 +110,38 @@ var basecam = { // Window pixels -> World (mouse picking) window_to_world(sx, sy) { var sr = this.screen_rect() - if (!rect_contains_pt(sr, sx, sy)) { - // outside letterbox bars; clamp or return null - // return null - } var u = (sx - sr.x) / sr.width - var v = (sy - sr.y) / sr.height + var v = 1 - (sy - sr.y) / sr.height return this.view_uv_to_world(u, v) }, // World -> normalized window [0..1] world_to_screen_norm(wx, wy) { var p = this.world_to_window(wx, wy) - return { x: p.x / gameres.width, y: p.y / gameres.height } + var ws = backend.get_window_size() + return { x: p.x / ws.width, y: p.y / ws.height } }, // Normalized window [0..1] -> World screen_norm_to_world(nx, ny) { - var sx = nx * gameres.width - var sy = ny * gameres.height + var ws = backend.get_window_size() + var sx = nx * ws.width + var sy = ny * ws.height return this.window_to_world(sx, sy) }, screen_to_world: function(sx, sy) { // sx, sy are normalized screen coordinates (0-1), bottom left [0,0], top right [1,1] var screen_rect = this.screen_rect() - var pixel_x = sx * gameres.width - var pixel_y = sy * gameres.height + var ws = backend.get_window_size() + var pixel_x = sx * ws.width + var pixel_y = sy * ws.height var rel_x = (pixel_x - screen_rect.x) / screen_rect.width var rel_y = (pixel_y - screen_rect.y) / screen_rect.height - var ax = this.anchor[0] - var ay = this.anchor[1] - var world_x = this.pos[0] + (rel_x - ax) * this.width - var world_y = this.pos[1] + (rel_y - ay) * this.height + var ax = this.anchor.x + var ay = this.anchor.y + var world_x = this.pos.x + (rel_x - ax) * this.width + var world_y = this.pos.y + (rel_y - ay) * this.height return {x: world_x, y: world_y} }, } diff --git a/compositor.cm b/compositor.cm index 541c26f1..98084726 100644 --- a/compositor.cm +++ b/compositor.cm @@ -123,9 +123,8 @@ function compile_plane(plane_config, ctx, group_effects) { // Allocate plane target var plane_target = ctx.alloc(res.width, res.height, plane_config.name) - // Clear plane - if (plane_config.clear) - push(ctx.passes, {type: 'clear', target: plane_target, color: plane_config.clear}) + // Always clear plane target to prevent stale data between frames + push(ctx.passes, {type: 'clear', target: plane_target, color: plane_config.clear || {r: 0, g: 0, b: 0, a: 0}}) // Render each effect group to temp target, apply effects, composite back arrfor(array(effect_groups), gname => { diff --git a/examples/bunnymark/main.ce b/examples/bunnymark/main.ce index bbaae0e1..1d99e52f 100644 --- a/examples/bunnymark/main.ce +++ b/examples/bunnymark/main.ce @@ -59,7 +59,7 @@ for (i = 0; i < 100; i++) { random.random() * GH ) } -count_label.text = "Bunnies: " + length(bunnies) +count_label.text = "Bunnies: " + text(length(bunnies)) // Mouse position tracking var mouse_x = GW / 2, mouse_y = GH / 2 @@ -85,12 +85,12 @@ core.start({ update: function(dt) { // Spawn bunnies while clicking var down = input.player1().down() + var si = 0 if (down.spawn) { - var si = 0 for (si = 0; si < 50; si++) { add_bunny(mouse_x, mouse_y) } - count_label.text = "Bunnies: " + length(bunnies) + count_label.text = "Bunnies: " + text(length(bunnies)) } // Update all bunnies @@ -112,7 +112,7 @@ core.start({ frame_count++ fps_timer += dt if (fps_timer >= 1) { - fps_label.text = "FPS: " + frame_count + fps_label.text = "FPS: " + text(frame_count) frame_count = 0 fps_timer -= 1 } diff --git a/examples/chess/chess.ce b/examples/chess/chess.ce index 2c3ad68c..882ad1b1 100644 --- a/examples/chess/chess.ce +++ b/examples/chess/chess.ce @@ -3,16 +3,16 @@ var camera = use('camera') var compositor = use('compositor') var input = use('input') var shape = use('shape2d') -var sprite_factory = use('sprite') var text2d = use('text2d') -var Grid = use('grid') -var MovementSystem = use('movement').MovementSystem -var startingPos = use('pieces').startingPosition -var rules = use('rules') +var Grid = use('examples/chess/grid') +var MovementSystem = use('examples/chess/movement') +var startingPos = use('examples/chess/pieces').startingPosition +var rules = use('examples/chess/rules') var S = 60 var GW = S * 8, GH = S * 8 +var move_history = [] var game_cam = camera.make({width: GW, height: GH, pos: {x: GW / 2, y: GH / 2}}) var hud_cam = camera.make({width: GW, height: GH, pos: {x: GW / 2, y: GH / 2}}) @@ -38,11 +38,11 @@ var select_color = {r: 1, g: 0.84, b: 0, a: 1} var valid_color = {r: 0.6, g: 0.8, b: 0.4, a: 1} var board_shapes = [] -var bx = 0, by = 0 +var bx = 0, by = 0, row = null, col = null for (by = 0; by < 8; by++) { - var row = [] + row = [] for (bx = 0; bx < 8; bx++) { - var col = ((bx + by) & 1) ? dark_color : light_color + col = ((bx + by) & 1) ? dark_color : light_color push(row, shape.rect({ pos: {x: bx * S + S / 2, y: by * S + S / 2}, width: S, height: S, @@ -53,17 +53,25 @@ for (by = 0; by < 8; by++) { push(board_shapes, row) } -// Piece sprites — one per piece, keyed by piece object -var piece_sprites = {} +// Piece letter abbreviations +var piece_letter = { + king: "K", queen: "Q", rook: "R", + bishop: "B", knight: "N", pawn: "P" +} +var white_color = {r: 1, g: 1, b: 1, a: 1} +var black_color = {r: 0.1, g: 0.1, b: 0.1, a: 1} + +// Piece labels — one per piece, keyed by piece id +var piece_labels = {} var piece_id = 0 grid.each(function(p) { piece_id++ p._id = piece_id - piece_sprites[piece_id] = sprite_factory({ - image: p.sprite, + piece_labels[text(piece_id)] = text2d({ + text: piece_letter[p.kind], pos: {x: p.coord[0] * S + S / 2, y: p.coord[1] * S + S / 2}, - width: S, height: S, - anchor_x: 0.5, anchor_y: 0.5, + size: 36, + color: (p.colour == 'white') ? white_color : black_color, plane: 'game', layer: 1 }) }) @@ -86,17 +94,27 @@ function update_status() { } function reset_board_colors() { - var x = 0, y = 0 + var x = 0, y = 0, col = null for (y = 0; y < 8; y++) { for (x = 0; x < 8; x++) { - var col = ((x + y) & 1) ? dark_color : light_color + col = ((x + y) & 1) ? dark_color : light_color board_shapes[y][x].fill = {r: col.r, g: col.g, b: col.b, a: col.a} } } } +var hover_color = {r: 0.8, g: 0.85, b: 0.6, a: 1} + function highlight_selection() { reset_board_colors() + + // Hover highlight + if (hover_gx >= 0 && hover_gx < 8 && hover_gy >= 0 && hover_gy < 8) { + board_shapes[hover_gy][hover_gx].fill = { + r: hover_color.r, g: hover_color.g, b: hover_color.b, a: hover_color.a + } + } + if (!selectPos) return // Highlight selected square @@ -105,9 +123,9 @@ function highlight_selection() { } // Highlight valid moves - var i = 0 + var i = 0, m = null for (i = 0; i < length(validMoves); i++) { - var m = validMoves[i] + m = validMoves[i] board_shapes[m[1]][m[0]].fill = { r: valid_color.r, g: valid_color.g, b: valid_color.b, a: valid_color.a } @@ -132,22 +150,24 @@ function compute_valid_moves(from) { } function sync_piece_sprites() { + var keys = array(piece_labels) + var i = 0 + for (i = 0; i < length(keys); i++) { + piece_labels[keys[i]].visible = false + } grid.each(function(p) { - var spr = piece_sprites[p._id] - if (!spr) return - if (p.captured) { - spr.visible = false - } else { - spr.pos.x = p.coord[0] * S + S / 2 - spr.pos.y = p.coord[1] * S + S / 2 - spr.visible = true - } + var lbl = piece_labels[text(p._id)] + if (!lbl) return + lbl.pos.x = p.coord[0] * S + S / 2 + lbl.pos.y = p.coord[1] * S + S / 2 + lbl.visible = true }) } // Input handler var game_input = { on_input: function(action, data) { + var clicked = null, cell = null, is_valid = false, i = 0, src_piece = null if (!data.pressed) return if (action == 'cancel') { @@ -158,13 +178,13 @@ var game_input = { } if (action == 'select' && hover_gx >= 0 && hover_gx < 8 && hover_gy >= 0 && hover_gy < 8) { - var clicked = [hover_gx, hover_gy] - var cell = grid.at(clicked) + clicked = [hover_gx, hover_gy] + cell = grid.at(clicked) if (selectPos) { // Try to move - var is_valid = false - var i = 0 + is_valid = false + i = 0 for (i = 0; i < length(validMoves); i++) { if (validMoves[i][0] == clicked[0] && validMoves[i][1] == clicked[1]) { is_valid = true @@ -173,8 +193,10 @@ var game_input = { } if (is_valid) { - var src_piece = grid.at(selectPos)[0] + src_piece = grid.at(selectPos)[0] if (src_piece && mover.tryMove(src_piece, clicked)) { + move_history[] = sq_name(selectPos[0], selectPos[1]) + "-" + sq_name(clicked[0], clicked[1]) + log.chess("move " + sq_name(selectPos[0], selectPos[1]) + "-" + sq_name(clicked[0], clicked[1]) + " turn=" + mover.turn) sync_piece_sprites() update_status() } @@ -201,6 +223,66 @@ var game_input = { } input.player1().possess(game_input) +// --- Probe endpoints for AI play --- +var probe = use('probe') + +var file_letters = "abcdefgh" +function sq_name(x, y) { + return text(file_letters, x, x + 1) + text(y + 1) +} + +function piece_char(p) { + var chars = {king: "K", queen: "Q", rook: "R", bishop: "B", knight: "N", pawn: "P"} + var c = chars[p.kind] + if (p.colour == 'black') c = lower(c) + return c +} + +probe.register("chess", { + board: function(a) { + var rows = [] + var y = 0, x = 0, cell = null, row = null + for (y = 7; y >= 0; y--) { + row = "" + for (x = 0; x < 8; x++) { + cell = grid.at([x, y]) + if (length(cell)) { + row = row + piece_char(cell[0]) + } else { + row = row + "." + } + if (x < 7) row = row + " " + } + rows[] = row + } + return {turn: mover.turn, moves: move_history, board: rows} + }, + + move: function(a) { + var fx = a.fx, fy = a.fy, tx = a.tx, ty = a.ty + if (is_null(fx) || is_null(fy) || is_null(tx) || is_null(ty)) + return {ok: false, error: "need fx fy tx ty"} + var from = [fx, fy] + var to = [tx, ty] + var cell = grid.at(from) + if (!length(cell)) + return {ok: false, error: "no piece at " + sq_name(fx, fy)} + var piece = cell[0] + if (piece.colour != mover.turn) + return {ok: false, error: "not " + piece.colour + "'s turn"} + if (!rules.canMove(piece, from, to, grid)) + return {ok: false, error: "illegal move"} + if (mover.tryMove(piece, to)) { + move_history[] = sq_name(fx, fy) + "-" + sq_name(tx, ty) + log.chess("move " + sq_name(fx, fy) + "-" + sq_name(tx, ty) + " turn=" + mover.turn) + sync_piece_sprites() + update_status() + return {ok: true, move: sq_name(fx, fy) + "-" + sq_name(tx, ty)} + } + return {ok: false, error: "move rejected"} + } +}) + var comp_config = { clear: {r: 0.15, g: 0.15, b: 0.2, a: 1}, planes: [ @@ -210,12 +292,13 @@ var comp_config = { } core.start({ - width: 640, height: 640, title: "Chess", + width: 640, height: 640, title: "Chess", probe: true, input: function(ev) { + var wp = null if (ev.type == 'mouse_motion') { // Convert pixel coords to grid coords via camera - var wp = game_cam.window_to_world(ev.pos.x, ev.pos.y) + wp = game_cam.window_to_world(ev.pos[0], ev.pos[1]) if (wp) { hover_gx = floor(wp.x / S) hover_gy = floor(wp.y / S) @@ -224,6 +307,7 @@ core.start({ }, update: function(dt) { + highlight_selection() }, render: function() { diff --git a/examples/chess/grid.cm b/examples/chess/grid.cm index 3f0971db..edc5d373 100644 --- a/examples/chess/grid.cm +++ b/examples/chess/grid.cm @@ -1,3 +1,6 @@ +function px(p) { return !is_null(p.x) ? p.x : p[0] } +function py(p) { return !is_null(p.y) ? p.y : p[1] } + function grid(w, h) { var newgrid = meme(grid_prototype) newgrid.width = w; @@ -23,25 +26,28 @@ var grid_prototype = { // alias for cell at(pos) { - return this.cell(pos.x, pos.y); + return this.cell(px(pos), py(pos)); }, // add an entity into a cell add(entity, pos) { - push(this.cell(pos.x, pos.y), entity); - entity.coord = array(pos); + var cx = px(pos), cy = py(pos); + push(this.cell(cx, cy), entity); + entity.coord = [cx, cy]; }, // remove an entity from a cell remove(entity, pos) { - this.cells[pos.y][pos.x] = filter(this.cells[pos.y][pos.x], x => x != entity) + var cx = px(pos), cy = py(pos); + this.cells[cy][cx] = filter(this.cells[cy][cx], x => x != entity) }, // bounds check inBounds(pos) { + var cx = px(pos), cy = py(pos); return ( - pos.x >= 0 && pos.x < this.width && - pos.y >= 0 && pos.y < this.height + cx >= 0 && cx < this.width && + cy >= 0 && cy < this.height ); }, @@ -54,7 +60,7 @@ var grid_prototype = { for (x = 0; x < this.width; x++) { list = this.cells[y][x] arrfor(list, function(entity) { - fn(entity, entity.coord); + fn(entity); }) } } @@ -67,7 +73,7 @@ var grid_prototype = { var x = 0; for (y = 0; y < this.height; y++) { for (x = 0; x < this.width; x++) { - out += length(this.cells[y][x]); + out += text(length(this.cells[y][x])); } if (y != this.height - 1) out += "\n"; } diff --git a/examples/chess/movement.cm b/examples/chess/movement.cm index e3a1d387..a90305a6 100644 --- a/examples/chess/movement.cm +++ b/examples/chess/movement.cm @@ -19,15 +19,14 @@ var MovementSystem_prototype = { var victims = this.grid.at(dest); if (length(victims) && victims[0].colour == piece.colour) return false; - if (length(victims)) victims[0].captured = true; + if (length(victims)) { + victims[0].captured = true; + this.grid.remove(victims[0], dest); + } this.grid.remove(piece, piece.coord); this.grid.add (piece, dest); - // grid.add() re-creates coord; re-add .x/.y fields: - piece.coord.x = dest.x; - piece.coord.y = dest.y; - this.turn = (this.turn == 'white') ? 'black' : 'white'; return true; } diff --git a/examples/chess/pieces.cm b/examples/chess/pieces.cm index ad8e60bb..4c4be4fe 100644 --- a/examples/chess/pieces.cm +++ b/examples/chess/pieces.cm @@ -14,14 +14,14 @@ function startingPosition(grid) { // pawns for (x = 0; x < 8; x++) { - grid.add(Piece('pawn', W), [x, 6]); - grid.add(Piece('pawn', B), [x, 1]); + grid.add(Piece('pawn', W), [x, 1]); + grid.add(Piece('pawn', B), [x, 6]); } // major pieces var back = ['rook','knight','bishop','queen','king','bishop','knight','rook']; for (x = 0; x < 8; x++) { - grid.add(Piece(back[x], W), [x, 7]); - grid.add(Piece(back[x], B), [x, 0]); + grid.add(Piece(back[x], W), [x, 0]); + grid.add(Piece(back[x], B), [x, 7]); } } diff --git a/examples/chess/rules.cm b/examples/chess/rules.cm index fa4b77f6..8a78608f 100644 --- a/examples/chess/rules.cm +++ b/examples/chess/rules.cm @@ -5,8 +5,8 @@ function cy(c) { return !is_null(c.y) ? c.y : c[1] } /* simple move-shape checks */ var deltas = { pawn: function (pc, dx, dy, ctx) { - var dir = (pc.colour == 'white') ? -1 : 1; - var base = (pc.colour == 'white') ? 6 : 1; + var dir = (pc.colour == 'white') ? 1 : -1; + var base = (pc.colour == 'white') ? 1 : 6; var one = (dy == dir && dx == 0 && length(ctx.grid.at(ctx.to)) == 0); var two = (dy == 2 * dir && dx == 0 && cy(pc.coord) == base && length(ctx.grid.at({ x: cx(pc.coord), y: cy(pc.coord)+dir })) == 0 && @@ -42,4 +42,4 @@ function canMove(piece, from, to, grid) { return clearLine(from, to, grid); } -return { canMove }; +return { canMove: canMove }; diff --git a/examples/pong/main.ce b/examples/pong/main.ce index c280a16b..10b17ac6 100644 --- a/examples/pong/main.ce +++ b/examples/pong/main.ce @@ -111,12 +111,12 @@ core.start({ // Scoring if (bx < 0) { score2++ - score_label.text = score1 + " " + score2 + score_label.text = text(score1) + " " + text(score2) reset_ball() } if (bx > GW) { score1++ - score_label.text = score1 + " " + score2 + score_label.text = text(score1) + " " + text(score2) reset_ball() } }, diff --git a/examples/snake/main.ce b/examples/snake/main.ce index b84e201e..3229e502 100644 --- a/examples/snake/main.ce +++ b/examples/snake/main.ce @@ -140,6 +140,7 @@ core.start({ // New head position var hx = snake_pos[0].x + dir.x var hy = snake_pos[0].y + dir.y + var tail = null // Wrap if (hx < 0) hx = gridW - 1 @@ -164,23 +165,27 @@ core.start({ // Add head with tween from old head position var old_wp = grid_to_world(snake_pos[0].x, snake_pos[0].y) var new_wp = grid_to_world(hx, hy) - snake_pos.unshift({x: hx, y: hy}) + var new_pos = [{x: hx, y: hy}] + for (i = 0; i < length(snake_pos); i++) push(new_pos, snake_pos[i]) + snake_pos = new_pos var head = shape.rect({ pos: {x: old_wp.x, y: old_wp.y}, width: cellSize - 2, height: cellSize - 2, fill: {r: 0, g: 1, b: 0.3, a: 1}, plane: 'game' }) // Smooth tween from old position to new position tw.tween(head.pos).to({x: new_wp.x, y: new_wp.y}, move_interval).ease(ease.linear) - snake_shapes.unshift(head) + var new_shapes = [head] + for (i = 0; i < length(snake_shapes); i++) push(new_shapes, snake_shapes[i]) + snake_shapes = new_shapes // Eat apple? if (hx == apple_gx && hy == apple_gy) { score++ - score_label.text = "Score: " + score + score_label.text = "Score: " + text(score) spawn_apple() } else { // Remove tail - var tail = pop(snake_shapes) + tail = pop(snake_shapes) tail.destroy() pop(snake_pos) } diff --git a/examples/tetris/main.ce b/examples/tetris/main.ce index e3c39ac0..ddcb874b 100644 --- a/examples/tetris/main.ce +++ b/examples/tetris/main.ce @@ -45,10 +45,10 @@ var board_shapes = [] function init_board() { board = [] board_shapes = [] - var r = 0, c = 0 + var r = 0, c = 0, row = null, srow = null for (r = 0; r < ROWS; r++) { - var row = [] - var srow = [] + row = [] + srow = [] for (c = 0; c < COLS; c++) { push(row, null) push(srow, shape.rect({ @@ -155,7 +155,7 @@ function rotate_blocks(blocks) { function clear_lines() { var lines = 0 - var r = ROWS - 1, c = 0, full = false, newRow = null + var r = ROWS - 1, c = 0, full = false, newRow = null, sr = 0 while (r >= 0) { full = true for (c = 0; c < COLS; c++) { @@ -164,7 +164,7 @@ function clear_lines() { if (full) { lines++ // Shift rows down - var sr = r + sr = r while (sr > 0) { for (c = 0; c < COLS; c++) { board[sr][c] = board[sr - 1][c] @@ -191,15 +191,15 @@ function clear_lines() { else if (lines == 4) score += 800 linesCleared += lines level = floor(linesCleared / 10) - score_label.text = "Score: " + score - level_label.text = "Level: " + level + score_label.text = "Score: " + text(score) + level_label.text = "Level: " + text(level) } function update_piece_shapes() { - var i = 0 + var i = 0, bx = 0, by = 0 for (i = 0; i < 4; i++) { - var bx = pieceX + piece.blocks[i][0] - var by = pieceY + piece.blocks[i][1] + bx = pieceX + piece.blocks[i][0] + by = pieceY + piece.blocks[i][1] piece_shapes[i].pos.x = bx * TILE + TILE / 2 piece_shapes[i].pos.y = by * TILE + TILE / 2 piece_shapes[i].fill = piece.color @@ -208,10 +208,10 @@ function update_piece_shapes() { } function update_next_display() { - var i = 0 + var i = 0, bx = 0, by = 0 for (i = 0; i < 4; i++) { - var bx = nextPiece.blocks[i][0] - var by = nextPiece.blocks[i][1] + bx = nextPiece.blocks[i][0] + by = nextPiece.blocks[i][1] next_shapes[i].pos.x = (COLS + 1) * TILE + bx * TILE + TILE / 2 next_shapes[i].pos.y = 20 + by * TILE + TILE / 2 next_shapes[i].fill = nextPiece.color @@ -220,13 +220,14 @@ function update_next_display() { } function spawn_piece() { + var i = 0 piece = nextPiece || random_shape() nextPiece = random_shape() pieceX = 3 pieceY = 0 if (collides(pieceX, pieceY, piece.blocks)) { gameOver = true - var i = 0 + i = 0 for (i = 0; i < 4; i++) piece_shapes[i].visible = false return } diff --git a/imgui.cpp b/imgui.cpp index aa437498..ebb7fb06 100644 --- a/imgui.cpp +++ b/imgui.cpp @@ -880,7 +880,7 @@ const JSCFunctionListEntry js_imgui_funcs[] = { MIST_FUNC_DEF(imgui, menu, 2), MIST_FUNC_DEF(imgui, menubar, 1), MIST_FUNC_DEF(imgui, mainmenubar, 1), - MIST_FUNC_DEF(imgui, menuitem, 3), + MIST_FUNC_DEF(imgui, menuitem, 4), // Input functions MIST_FUNC_DEF(imgui, textinput, 2), diff --git a/input.cm b/input.cm index fdaec932..c7e498f5 100644 --- a/input.cm +++ b/input.cm @@ -83,7 +83,7 @@ function create_user(index, config) { // Get action down state down() { - return this.router ? this.router.down : {} + return this.router ? this.router.down() : {} }, // Possess an entity (clears stack, sets as sole target) diff --git a/sdl_gpu.cm b/sdl_gpu.cm index 9c7e2b3a..188a119c 100644 --- a/sdl_gpu.cm +++ b/sdl_gpu.cm @@ -1285,7 +1285,7 @@ function _execute_commands(commands, window_size) { current_pass = null } - _do_composite(cmd_buffer, cmd, window_size) + _do_composite(cmd_buffer, cmd) } else if (cmd.cmd == 'end_render') { // Flush pending draws @@ -1960,7 +1960,7 @@ function _get_font_cache(path, size, mode) { font =staef.sdf_font(data, size, 12.0, 14, 1.0) } else { // Bitmap - font =staef.font(data, size, false) + font =staef.font(data, size) } if (!font) { log.console(`sdl_gpu: Failed to load font ${path}:${size}:${local_mode}`) diff --git a/shaders/msl/text_msdf.frag.msl b/shaders/msl/text_msdf.frag.msl new file mode 100644 index 00000000..c952b943 --- /dev/null +++ b/shaders/msl/text_msdf.frag.msl @@ -0,0 +1,64 @@ +#include +using namespace metal; + +struct VertexOut { + float4 position [[position]]; + float2 uv; + float4 color; +}; + +struct Uniforms { + float outline_width; // Outline width in normalized SDF units (0.0 = no outline, 0.1-0.3 typical) + float sharpness; // Sharpness multiplier (1.0 = normal, higher = sharper) + float2 _pad; // Padding for alignment + float4 outline_color; // Outline color RGBA +}; + +// Median of three values - used to combine MSDF channels +float median(float r, float g, float b) { + return max(min(r, g), min(max(r, g), b)); +} + +fragment float4 fragment_main(VertexOut in [[stage_in]], + texture2d tex [[texture(0)]], + sampler smp [[sampler(0)]], + constant Uniforms &u [[buffer(0)]]) { + // Sample RGB channels from MSDF texture + float3 msdf = tex.sample(smp, in.uv).rgb; + + // Compute signed distance from the median of the three channels + float dist = median(msdf.r, msdf.g, msdf.b); + + // Edge is at 0.5 (where distance = 0 in the original SDF) + float edge = 0.5; + + // Calculate anti-aliasing width based on screen-space derivatives + // MSDF typically needs tighter AA than single-channel SDF + float aa = fwidth(dist); + float sharpness = max(u.sharpness, 0.1); + aa = aa / sharpness; + + // Fill coverage: inside the glyph + float fill = smoothstep(edge - aa, edge + aa, dist); + + // Outline coverage: extends outward from the edge + float outline_coverage = 0.0; + if (u.outline_width > 0.0) { + float outline_edge = edge - u.outline_width; + outline_coverage = smoothstep(outline_edge - aa, outline_edge + aa, dist); + } + + // Total coverage is the union of fill and outline + float total_coverage = max(fill, outline_coverage); + + // Blend colors: outline where not filled, fill color where filled + float3 rgb = in.color.rgb; + if (u.outline_width > 0.0) { + rgb = mix(u.outline_color.rgb, in.color.rgb, fill); + } + + // Final alpha combines coverage with vertex alpha + float a = total_coverage * in.color.a; + + return float4(rgb, a); +} diff --git a/shaders/msl/text_sdf.frag.msl b/shaders/msl/text_sdf.frag.msl new file mode 100644 index 00000000..fcf175ea --- /dev/null +++ b/shaders/msl/text_sdf.frag.msl @@ -0,0 +1,59 @@ +#include +using namespace metal; + +struct VertexOut { + float4 position [[position]]; + float2 uv; + float4 color; +}; + +struct Uniforms { + float outline_width; // Outline width in normalized SDF units (0.0 = no outline, 0.1-0.3 typical) + float sharpness; // Sharpness multiplier (1.0 = normal, higher = sharper) + float2 _pad; // Padding for alignment + float4 outline_color; // Outline color RGBA +}; + +fragment float4 fragment_main(VertexOut in [[stage_in]], + texture2d tex [[texture(0)]], + sampler smp [[sampler(0)]], + constant Uniforms &u [[buffer(0)]]) { + // Sample distance from alpha channel (SDF fonts store distance in alpha) + float dist = tex.sample(smp, in.uv).a; + + // Edge is at 0.5 (where distance = 0 in the original SDF) + float edge = 0.5; + + // Calculate anti-aliasing width based on screen-space derivatives + // Sharpness multiplier controls how tight the AA band is + float aa = fwidth(dist); + float sharpness = max(u.sharpness, 0.1); // Prevent division issues + aa = aa / sharpness; + + // Fill coverage: inside the glyph + float fill = smoothstep(edge - aa, edge + aa, dist); + + // Outline coverage: extends outward from the edge + float outline_coverage = 0.0; + if (u.outline_width > 0.0) { + // Outline edge is further out (lower distance value = further from glyph center) + float outline_edge = edge - u.outline_width; + outline_coverage = smoothstep(outline_edge - aa, outline_edge + aa, dist); + } + + // Total coverage is the union of fill and outline + float total_coverage = max(fill, outline_coverage); + + // Blend colors: outline where not filled, fill color where filled + // fill goes from 0 (outside glyph) to 1 (inside glyph) + // outline_coverage goes from 0 (outside outline) to 1 (inside outline+glyph) + float3 rgb = in.color.rgb; + if (u.outline_width > 0.0) { + rgb = mix(u.outline_color.rgb, in.color.rgb, fill); + } + + // Final alpha combines coverage with vertex alpha + float a = total_coverage * in.color.a; + + return float4(rgb, a); +}