diff --git a/action.cm b/action.cm index 1b2f4eb8..7d6f9d2c 100644 --- a/action.cm +++ b/action.cm @@ -25,55 +25,60 @@ action.get_icon_for_action = function(action) { var bindings = this.get_bindings_for_device(action) if (!length(bindings)) return null - + var primary_binding = bindings[0] - + var button = null + var key_mapping = { + 'escape': 'escape', + 'return': 'return', + 'space': 'space', + 'up': 'arrow_up', + 'down': 'arrow_down', + 'left': 'arrow_left', + 'right': 'arrow_right' + } + var key = null + var controller_prefix = null + var gamepad_mapping = null + var icon_name = null + if (this.current_device == 'keyboard') { if (starts_with(primary_binding, 'mouse_button_')) { - var button = replace(primary_binding, 'mouse_button_', '') + button = replace(primary_binding, 'mouse_button_', '') return 'ui/mouse/mouse_' + button + '.png' } else { // Handle special keyboard keys - var key_mapping = { - 'escape': 'escape', - 'return': 'return', - 'space': 'space', - 'up': 'arrow_up', - 'down': 'arrow_down', - 'left': 'arrow_left', - 'right': 'arrow_right' - } - var key = key_mapping[primary_binding] || primary_binding + key = key_mapping[primary_binding] || primary_binding return 'ui/keyboard/keyboard_' + key + '.png' } } - + if (this.current_device == 'gamepad' && this.current_gamepad_type) { - var controller_prefix = controller_map[this.current_gamepad_type] || 'playstation' - + controller_prefix = controller_map[this.current_gamepad_type] || 'playstation' + // Map gamepad inputs to icon names - var gamepad_mapping = { + gamepad_mapping = { 'gamepad_a': this.current_gamepad_type == 'ps5' || this.current_gamepad_type == 'ps4' || this.current_gamepad_type == 'ps3' ? 'playstation_button_cross' : 'xbox_button_a', - 'gamepad_b': this.current_gamepad_type == 'ps5' || this.current_gamepad_type == 'ps4' || this.current_gamepad_type == 'ps3' ? 'playstation_button_circle' : 'xbox_button_b', + 'gamepad_b': this.current_gamepad_type == 'ps5' || this.current_gamepad_type == 'ps4' || this.current_gamepad_type == 'ps3' ? 'playstation_button_circle' : 'xbox_button_b', 'gamepad_x': this.current_gamepad_type == 'ps5' || this.current_gamepad_type == 'ps4' || this.current_gamepad_type == 'ps3' ? 'playstation_button_square' : 'xbox_button_x', 'gamepad_y': this.current_gamepad_type == 'ps5' || this.current_gamepad_type == 'ps4' || this.current_gamepad_type == 'ps3' ? 'playstation_button_triangle' : 'xbox_button_y', 'gamepad_dpup': controller_prefix + '_dpad_up', - 'gamepad_dpdown': controller_prefix + '_dpad_down', + 'gamepad_dpdown': controller_prefix + '_dpad_down', 'gamepad_dpleft': controller_prefix + '_dpad_left', 'gamepad_dpright': controller_prefix + '_dpad_right', 'gamepad_l1': controller_prefix + '_trigger_l1', - 'gamepad_r1': controller_prefix + '_trigger_r1', + 'gamepad_r1': controller_prefix + '_trigger_r1', 'gamepad_l2': controller_prefix + '_trigger_l2', 'gamepad_r2': controller_prefix + '_trigger_r2', 'gamepad_start': this.get_start_button_icon() } - - var icon_name = gamepad_mapping[primary_binding] + + icon_name = gamepad_mapping[primary_binding] if (icon_name) { return 'ui/' + controller_prefix + '/' + icon_name + '.png' } } - + return null } @@ -181,23 +186,26 @@ var SWIPE_MAX_TIME = 500 // ms action.on_input = function(action_id, evt) { // 1) Detect & store which device user is on (only from raw input, ignore passive inputs) + var new_device = null + var is_real_kb_mouse = false + var gamepad_type = null if (action_id != 'mouse_move' && !starts_with(action_id, 'mouse_pos') && evt.pressed) { - var new_device = detect_device(action_id) - + new_device = detect_device(action_id) + // For keyboard/mouse detection, also check if the event has modifier keys or is a mouse button // This helps distinguish real keyboard/mouse events from mapped actions - var is_real_kb_mouse = (new_device == 'keyboard' && - (evt.ctrl != null || evt.shift != null || evt.alt != null || - starts_with(action_id, 'mouse_button'))) - + is_real_kb_mouse = (new_device == 'keyboard' && + (evt.ctrl != null || evt.shift != null || evt.alt != null || + starts_with(action_id, 'mouse_button'))) + if (new_device == 'keyboard' && !is_real_kb_mouse) { // This might be a mapped action, not a real keyboard/mouse event return } - + if (new_device != this.current_device) { if (new_device == 'gamepad') { - var gamepad_type = evt.which != null ? input.gamepad_id_to_type(evt.which) : 'unknown' + gamepad_type = evt.which != null ? input.gamepad_id_to_type(evt.which) : 'unknown' log.console("Input switched to 'gamepad' (event: " + action_id + ", type: " + gamepad_type + ")") this.current_gamepad_type = gamepad_type } else if (this.current_device == 'gamepad') { @@ -236,8 +244,9 @@ action.on_input = function(action_id, evt) }) // Send all matched actions (only if we found mappings - this means it's a raw input) + var i = 0 if (length(matched_actions) > 0) { - for (var i = 0; i < length(matched_actions); i++) { + for (i = 0; i < length(matched_actions); i++) { // scene.recurse(game.root, 'on_input', [matched_actions[i], evt]) } } @@ -261,7 +270,8 @@ action.rebind_action = function(action_name, new_key) { // Clear existing bindings for the current device from the target action var target_bindings = this.action_map[action_name] - for (var i = length(target_bindings) - 1; i >= 0; i--) { + var i = length(target_bindings) - 1 + for (; i >= 0; i--) { if (detect_device(target_bindings[i]) == this.current_device) this.action_map[action_name] = array(array(this.action_map[action_name], 0, i), array(this.action_map[action_name], i+1)) } @@ -299,22 +309,28 @@ action.get_current_device_binding = function(action_name) { } action.save_bindings = function() { - try { - io.slurpwrite('keybindings.json', json.encode(this.action_map)) - } catch(e) { - log.console("Failed to save key bindings:", e) + var self = this + var _save = function() { + io.slurpwrite('keybindings.json', json.encode(self.action_map)) + } disruption { + log.console("Failed to save key bindings") } + _save() } action.load_bindings = function() { - try { + var self = this + var _load = function() { + var data = null + var bindings = null if (io.exists('keybindings.json')) { - var data = io.slurp('keybindings.json') - var bindings = object(json.decode(data), this.action_map) + data = io.slurp('keybindings.json') + bindings = object(json.decode(data), self.action_map) } - } catch(e) { - log.console("Failed to load key bindings:", e) + } disruption { + log.console("Failed to load key bindings") } + _load() } action.reset_to_defaults = function() { diff --git a/clay.cm b/clay.cm index 28b1f4c0..36c97cd7 100644 --- a/clay.cm +++ b/clay.cm @@ -38,13 +38,13 @@ var base_config = { } function normalize_color(c, fallback) { - fallback = fallback || {r:1, g:1, b:1, a:1} - if (!c) return {r:fallback.r, g:fallback.g, b:fallback.b, a:fallback.a} + var fb = fallback || {r:1, g:1, b:1, a:1} + if (!c) return {r:fb.r, g:fb.g, b:fb.b, a:fb.a} return { - r: c.r != null ? c.r : fallback.r, - g: c.g != null ? c.g : fallback.g, - b: c.b != null ? c.b : fallback.b, - a: c.a != null ? c.a : fallback.a + r: c.r != null ? c.r : fb.r, + g: c.g != null ? c.g : fb.g, + b: c.b != null ? c.b : fb.b, + a: c.a != null ? c.a : fb.a } } @@ -59,8 +59,8 @@ function normalize_spacing(s) { } // Tree building state -var root_item -var tree_root +var root_item = null +var tree_root = null var config_stack = [] // Rewriting state management for cleaner recursion @@ -89,11 +89,11 @@ clay.layout = function(fn, size) { return build_drawables(root_node, size.height) } -function build_drawables(node, root_height, parent_abs_x, parent_abs_y, parent_scissor, parent_layer) { - parent_abs_x = parent_abs_x || 0 - parent_abs_y = parent_abs_y || 0 - parent_layer = parent_layer || 0 // UI usually on top, but let's start at 0 - +function build_drawables(node, root_height, parent, parent_scissor) { + var p_abs_x = (parent && parent.x) || 0 + var p_abs_y = (parent && parent.y) || 0 + var p_layer = (parent && parent.layer) || 0 + var rect = lay_ctx.get_rect(node.id) // Calculate absolute world Y for this node (bottom-up layout to top-down render) @@ -108,22 +108,23 @@ function build_drawables(node, root_height, parent_abs_x, parent_abs_y, parent_s // Scissor var current_scissor = parent_scissor + var sx = vis_x + var sy = vis_y + var sw = rect.width + var sh = rect.height + var clip_right = null + var clip_bottom = null if (node.config.clipped) { - var sx = vis_x - var sy = vis_y - var sw = rect.width - var sh = rect.height - // Intersect with parent if (parent_scissor) { sx = max(sx, parent_scissor.x) sy = max(sy, parent_scissor.y) - var right = min(vis_x + sw, parent_scissor.x + parent_scissor.width) - var bottom = min(vis_y + sh, parent_scissor.y + parent_scissor.height) - sw = max(0, right - sx) - sh = max(0, bottom - sy) + clip_right = min(vis_x + sw, parent_scissor.x + parent_scissor.width) + clip_bottom = min(vis_y + sh, parent_scissor.y + parent_scissor.height) + sw = max(0, clip_right - sx) + sh = max(0, clip_bottom - sy) } - + current_scissor = {x: sx, y: sy, width: sw, height: sh} } @@ -138,7 +139,7 @@ function build_drawables(node, root_height, parent_abs_x, parent_abs_y, parent_s height: rect.height, slice: node.config.slice, color: node.config.background_color || {r:1, g:1, b:1, a:1}, - layer: parent_layer - 0.1, // slightly behind content + layer: p_layer - 0.1, // slightly behind content scissor: current_scissor }) } else { @@ -149,7 +150,7 @@ function build_drawables(node, root_height, parent_abs_x, parent_abs_y, parent_s width: rect.width, height: rect.height, color: node.config.background_color || {r:1, g:1, b:1, a:1}, - layer: parent_layer - 0.1, + layer: p_layer - 0.1, scissor: current_scissor }) } @@ -160,7 +161,7 @@ function build_drawables(node, root_height, parent_abs_x, parent_abs_y, parent_s width: rect.width, height: rect.height, color: node.config.background_color, - layer: parent_layer - 0.1, + layer: p_layer - 0.1, scissor: current_scissor }) } @@ -174,7 +175,7 @@ function build_drawables(node, root_height, parent_abs_x, parent_abs_y, parent_s width: rect.width, height: rect.height, color: node.config.color, - layer: parent_layer, + layer: p_layer, scissor: current_scissor }) } @@ -196,14 +197,14 @@ function build_drawables(node, root_height, parent_abs_x, parent_abs_y, parent_s // `abs_y = root_height - (rect.y + rect.height)` -> Top edge of element. // Text usually wants baseline. // If we put it at `vis_y + rect.height`, that's bottom of element. - layer: parent_layer, + layer: p_layer, scissor: current_scissor }) } // Children arrfor(node.children, function(child) { - drawables = array(drawables, build_drawables(child, root_height, vis_x, vis_y, current_scissor, parent_layer + 0.01)) + drawables = array(drawables, build_drawables(child, root_height, {x: vis_x, y: vis_y, layer: p_layer + 0.01}, current_scissor)) }) return drawables @@ -235,8 +236,8 @@ function push_node(configs, contain_mode) { lay_ctx.set_contain(item, config.contain) lay_ctx.set_behave(item, config.behave) - if (config.size) { - var s = config.size + var s = config.size + if (s) { if (is_array(s)) s = {width: s[0], height: s[1]} lay_ctx.set_size(item, s) } @@ -262,44 +263,44 @@ function pop_node() { // Generic container clay.container = function(configs, fn) { - if (is_function(configs)) { fn = configs; configs = {} } - if (!is_array(configs)) configs = [configs] - - push_node(configs, null) - if (fn) fn() + var _fn = is_function(configs) ? configs : fn + var _configs = is_function(configs) ? [{}] : (is_array(configs) ? configs : [configs]) + + push_node(_configs, null) + if (_fn) _fn() pop_node() } // Stacks clay.vstack = function(configs, fn) { - if (is_function(configs)) { fn = configs; configs = {} } - if (!is_array(configs)) configs = [configs] - + var _fn = is_function(configs) ? configs : fn + var _configs = is_function(configs) ? [{}] : (is_array(configs) ? configs : [configs]) + var c = layout.contain.column - - push_node(configs, c) - if (fn) fn() + + push_node(_configs, c) + if (_fn) _fn() pop_node() } clay.hstack = function(configs, fn) { - if (is_function(configs)) { fn = configs; configs = {} } - if (!is_array(configs)) configs = [configs] - + var _fn = is_function(configs) ? configs : fn + var _configs = is_function(configs) ? [{}] : (is_array(configs) ? configs : [configs]) + var c = layout.contain.row - push_node(configs, c) - if (fn) fn() + push_node(_configs, c) + if (_fn) _fn() pop_node() } clay.zstack = function(configs, fn) { - if (is_function(configs)) { fn = configs; configs = {} } - if (!is_array(configs)) configs = [configs] - - var c = layout.contain.layout - - push_node(configs, c) - if (fn) fn() + var _fn = is_function(configs) ? configs : fn + var _configs = is_function(configs) ? [{}] : (is_array(configs) ? configs : [configs]) + + var c = layout.contain.layout + + push_node(_configs, c) + if (_fn) _fn() pop_node() } @@ -308,12 +309,12 @@ clay.image = function(path, configs) { var img = graphics.texture(path) var c = [{image: path}] var final_config = process_configs(configs) - if (!final_config.size && !final_config.behave) + if (!final_config.size && !final_config.behave) c.size = {width: img.width, height: img.height} - if (!is_array(configs)) configs = [configs] - - push_node(array(c, configs), null) + var _configs = is_array(configs) ? configs : [configs] + + push_node(array(c, _configs), null) pop_node() } @@ -321,18 +322,18 @@ clay.text = function(str, configs) { var c = [{text: str}] var final_config = process_configs(configs) if (!final_config.size && !final_config.behave) { - c.size = {width: 100, height: 20} + c.size = {width: 100, height: 20} } - if (!is_array(configs)) configs = [configs] - - push_node(array(c, configs), null) + var _configs = is_array(configs) ? configs : [configs] + + push_node(array(c, _configs), null) pop_node() } clay.rectangle = function(configs) { - if (!is_array(configs)) configs = [configs] - push_node(configs, null) + var _configs = is_array(configs) ? configs : [configs] + push_node(_configs, null) pop_node() } @@ -341,10 +342,10 @@ clay.button = function(str, action, configs) { padding: 10, background_color: {r:0.3, g:0.3, b:0.4, a:1} }] - - if (!is_array(configs)) configs = [configs] - - clay.zstack(array(btn_config, configs), function() { + + var _configs = is_array(configs) ? configs : [configs] + + clay.zstack(array(btn_config, _configs), function() { clay.text(str, {color: {r:1,g:1,b:1,a:1}}) }) } diff --git a/clay_input.cm b/clay_input.cm index bc6a42ad..a641d21a 100644 --- a/clay_input.cm +++ b/clay_input.cm @@ -35,12 +35,16 @@ function find_path(node, path, pos) { if (!rect_contains(node, pos)) return null var next_path = array(path, node) + var i = 0 + var child = null + var child_path = null if (node[clay.CHILDREN] && !should_skip_children(node)) { // Children drawn later should be tested first; reverse if your render order differs - for (var i = length(node[clay.CHILDREN]) - 1; i >= 0; i--) { - var child = node[clay.CHILDREN][i] - var child_path = find_path(child, next_path, pos) + i = length(node[clay.CHILDREN]) - 1 + for (; i >= 0; i--) { + child = node[clay.CHILDREN][i] + child_path = find_path(child, next_path, pos) if (child_path) return child_path } } @@ -65,7 +69,8 @@ clay_input.bubble = function bubble(deepest, prop) { return null } -clay_input.click = function click(tree_root, mousepos, button = 'left') { +clay_input.click = function click(tree_root, mousepos, button) { + var _button = button || 'left' var deepest = clay_input.deepest(tree_root, mousepos) var action_target = clay_input.bubble(deepest, 'action') if (action_target && action_target.config.action) action_target.config.action() diff --git a/color.cm b/color.cm index 8153543d..7665cfef 100644 --- a/color.cm +++ b/color.cm @@ -181,24 +181,30 @@ ColorMap.Viridis = ColorMap.makemap({ Color.normalize(ColorMap); -ColorMap.sample = function(t, map = this) { - if (t < 0) return map[0] - if (t > 1) return map[1] +ColorMap.sample = function(t, map) { + var local_map = map || this + if (t < 0) return local_map[0] + if (t > 1) return local_map[1] - var keys = sorted(array(map)) + var keys = sorted(array(local_map)) var lastkey = 0 + var i = 0 + var key = null + var b = null + var a = null + var tt = 0 - for (var i = 0; i < length(keys); i++) { - var key = keys[i] + for (i = 0; i < length(keys); i++) { + key = keys[i] if (t < key) { - var b = map[key] - var a = map[lastkey] - var tt = (t - lastkey) / (key - lastkey) + b = local_map[key] + a = local_map[lastkey] + tt = (t - lastkey) / (key - lastkey) return a.lerp(b, tt) } lastkey = key } - return map[1] + return local_map[1] } ColorMap.doc = { diff --git a/compositor.cm b/compositor.cm index a9d34509..64e7a824 100644 --- a/compositor.cm +++ b/compositor.cm @@ -27,9 +27,12 @@ compositor.compile = function(config) { // Process each plane (supports both 'planes' and legacy 'layers' key) var planes = config.planes || config.layers || [] - for (var i = 0; i < length(planes); i++) { - var plane = planes[i] - var type = plane.type || 'film2d' + var i = 0 + var plane = null + var type = null + for (i = 0; i < length(planes); i++) { + plane = planes[i] + type = plane.type || 'film2d' if (type == 'imgui') { compile_imgui_layer(plane, ctx) } else { @@ -58,7 +61,8 @@ function compile_plane(plane_config, ctx, group_effects) { var mask_groups = {} arrfor(array(group_effects), gname => { var effects = group_effects[gname].effects || [] - for (var e = 0; e < length(effects); e++) { + var e = 0 + for (e = 0; e < length(effects); e++) { if (effects[e].type == 'mask' && effects[e].mask_group) mask_groups[effects[e].mask_group] = true } @@ -68,33 +72,41 @@ function compile_plane(plane_config, ctx, group_effects) { var all_sprites = film2d.query({plane: plane_name}) // Add manual drawables + var di = 0 if (plane_config.drawables) { - for (var i = 0; i < length(plane_config.drawables); i++) - push(all_sprites, plane_config.drawables[i]) + for (di = 0; di < length(plane_config.drawables); di++) + push(all_sprites, plane_config.drawables[di]) } // Find which sprites belong to groups with effects var effect_groups = {} // group_name -> {sprites: [], effects: []} var base_sprites = [] - - for (var i = 0; i < length(all_sprites); i++) { - var s = all_sprites[i] - var sprite_groups = s.groups || [] - var assigned = false - var is_mask_only = length(sprite_groups) > 0 - + + var si = 0 + var s = null + var sprite_groups = null + var assigned = false + var is_mask_only = false + var g = 0 + var gname = null + for (si = 0; si < length(all_sprites); si++) { + s = all_sprites[si] + sprite_groups = s.groups || [] + assigned = false + is_mask_only = length(sprite_groups) > 0 + // First pass: check if sprite has any non-mask group - for (var g = 0; g < length(sprite_groups); g++) { - var gname = sprite_groups[g] + for (g = 0; g < length(sprite_groups); g++) { + gname = sprite_groups[g] if (!mask_groups[gname]) { is_mask_only = false break } } - + // Second pass: assign to effect groups - for (var g = 0; g < length(sprite_groups); g++) { - var gname = sprite_groups[g] + for (g = 0; g < length(sprite_groups); g++) { + gname = sprite_groups[g] if (group_effects[gname]) { if (!effect_groups[gname]) effect_groups[gname] = {sprites: [], effects: group_effects[gname].effects} @@ -103,7 +115,7 @@ function compile_plane(plane_config, ctx, group_effects) { break // Only assign to first matching effect group } } - + // Add to base sprites if not assigned to effect group and not mask-only if (!assigned && !is_mask_only) push(base_sprites, s) } @@ -136,9 +148,11 @@ function compile_plane(plane_config, ctx, group_effects) { // Apply effects var current = group_target - for (var e = 0; e < length(eg.effects); e++) { - var effect = eg.effects[e] - current = apply_effect(ctx, effect, current, res, camera, gname, plane_name, group_effects) + var e = 0 + var effect = null + for (e = 0; e < length(eg.effects); e++) { + effect = eg.effects[e] + current = apply_effect(ctx, effect, current, {size: res, camera: camera, hint: gname, current_plane: plane_name, group_effects: group_effects}) } // Composite result to plane @@ -176,14 +190,28 @@ function compile_plane(plane_config, ctx, group_effects) { }) } -function apply_effect(ctx, effect, input, size, camera, hint, current_plane, group_effects) { +function apply_effect(ctx, effect, input, params) { + var size = params.size + var camera = params.camera + var hint = params.hint + var current_plane = params.current_plane + var group_effects = params.group_effects var output = ctx.alloc(size.width, size.height, hint + '_' + effect.type) - + + var bright = null + var blur1 = null + var blur2 = null + var blur_passes = null + var blur_in = null + var p = 0 + var mask_group = null + var mask_sprites = null + var mask_target = null if (effect.type == 'bloom') { - var bright = ctx.alloc(size.width, size.height, hint + '_bright') - var blur1 = ctx.alloc(size.width, size.height, hint + '_blur1') - var blur2 = ctx.alloc(size.width, size.height, hint + '_blur2') - + bright = ctx.alloc(size.width, size.height, hint + '_bright') + blur1 = ctx.alloc(size.width, size.height, hint + '_blur1') + blur2 = ctx.alloc(size.width, size.height, hint + '_blur2') + // Threshold push(ctx.passes, { type: 'shader_pass', @@ -192,11 +220,11 @@ function apply_effect(ctx, effect, input, size, camera, hint, current_plane, gro output: bright, uniforms: {threshold: effect.threshold || 0.8, intensity: effect.intensity || 1} }) - + // Blur passes - var blur_passes = effect.blur_passes || 2 - var blur_in = bright - for (var p = 0; p < blur_passes; p++) { + blur_passes = effect.blur_passes || 2 + blur_in = bright + for (p = 0; p < blur_passes; p++) { push(ctx.passes, {type: 'shader_pass', shader: 'blur', input: blur_in, output: blur1, uniforms: {direction: {x: 1, y: 0}, texel_size: {x: 1/size.width, y: 1/size.height}}}) push(ctx.passes, {type: 'shader_pass', shader: 'blur', input: blur1, output: blur2, uniforms: {direction: {x: 0, y: 1}, texel_size: {x: 1/size.width, y: 1/size.height}}}) blur_in = blur2 @@ -206,12 +234,12 @@ function apply_effect(ctx, effect, input, size, camera, hint, current_plane, gro push(ctx.passes, {type: 'composite_textures', base: input, overlay: blur2, output: output, mode: 'add'}) } else if (effect.type == 'mask') { - var mask_group = effect.mask_group + mask_group = effect.mask_group // Query masks within the same plane to avoid cross-plane mask issues - var mask_sprites = film2d.query({group: mask_group, plane: current_plane}) - + mask_sprites = film2d.query({group: mask_group, plane: current_plane}) + if (length(mask_sprites) > 0) { - var mask_target = ctx.alloc(size.width, size.height, hint + '_mask') + mask_target = ctx.alloc(size.width, size.height, hint + '_mask') // Render mask push(ctx.passes, { @@ -261,17 +289,25 @@ compositor.execute = function(plan) { } var commands = [] - - for (var i = 0; i < length(plan.passes); i++) { - var pass = plan.passes[i] - + + var i = 0 + var pass = null + var target = null + var result = null + var c = 0 + var rect = null + var src = null + var dst = null + for (i = 0; i < length(plan.passes); i++) { + pass = plan.passes[i] + if (pass.type == 'clear') { - var target = resolve(pass.target) + target = resolve(pass.target) push(commands, {cmd: 'begin_render', target: target, clear: pass.color}) push(commands, {cmd: 'end_render'}) - + } else if (pass.type == 'render') { - var result = film2d.render({ + result = film2d.render({ drawables: pass.drawables, camera: pass.camera, target: resolve(pass.target), @@ -279,7 +315,7 @@ compositor.execute = function(plan) { layer_sort: pass.layer_sort || {}, clear: pass.clear }, backend) - for (var c = 0; c < length(result.commands); c++) + for (c = 0; c < length(result.commands); c++) push(commands, result.commands[c]) } else if (pass.type == 'shader_pass') { @@ -319,7 +355,7 @@ compositor.execute = function(plan) { }) } else if (pass.type == 'blit_to_screen') { - var rect = _calc_presentation(pass.source_size, pass.dest_size, pass.presentation) + rect = _calc_presentation(pass.source_size, pass.dest_size, pass.presentation) push(commands, { cmd: 'blit', texture: resolve(pass.source), @@ -328,8 +364,8 @@ compositor.execute = function(plan) { filter: pass.presentation == 'integer_scale' ? 'nearest' : 'linear' }) } else if (pass.type == 'blit') { - var src = resolve(pass.source) - var dst = resolve(pass.dest) + src = resolve(pass.source) + dst = resolve(pass.dest) push(commands, { cmd: 'blit', texture: src, @@ -351,20 +387,26 @@ compositor.execute = function(plan) { function _calc_presentation(src, dst, mode) { if (mode == 'stretch') return {x: 0, y: 0, width: dst.width, height: dst.height} - + + var sx = 0 + var sy = 0 + var s = 0 + var w = 0 + var h = 0 + var scale = 0 if (mode == 'integer_scale') { - var sx = floor(dst.width / src.width) - var sy = floor(dst.height / src.height) - var s = max(1, min(sx, sy)) - var w = src.width * s - var h = src.height * s + 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 - var scale = min(dst.width / src.width, dst.height / src.height) - var w = src.width * scale - var h = src.height * scale + 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} } diff --git a/core.cm b/core.cm index 359267a5..f8eaf3c7 100644 --- a/core.cm +++ b/core.cm @@ -84,11 +84,12 @@ var _fps_sample_pos = 0 function fps_add_sample(sample) { var n = length(_fps_samples) + var old = null if (n < _fps_sample_count) { push(_fps_samples, sample) _fps_sample_sum += sample } else { - var old = _fps_samples[_fps_sample_pos] + old = _fps_samples[_fps_sample_pos] _fps_samples[_fps_sample_pos] = sample _fps_sample_sum += sample - old _fps_sample_pos++ @@ -165,22 +166,25 @@ function _main_loop() { } // Render + var render_result = null + var dbg = false + var stats = null if (_config.render) { - var render_result = _config.render() + render_result = _config.render() if (render_result) { if (_config.debug == 'graph') { log.console(render_result) $stop() return } - var dbg = _config.debug == 'cmd' - + dbg = _config.debug == 'cmd' + // Build stats for debug_imgui - var stats = { + stats = { fps: _current_fps, frame_time_ms: _frame_time_ms } - + // Handle both compositor result ({commands: [...]}) and fx_graph (graph object) if (render_result.commands) { if (_config.imgui || _config.editor) { diff --git a/debug_imgui.cm b/debug_imgui.cm index ad4d4832..db3a4e3b 100644 --- a/debug_imgui.cm +++ b/debug_imgui.cm @@ -117,14 +117,15 @@ function _render_node_tree(imgui, node, depth) { } var has_children = node.children && length(node.children) > 0 - + if (has_children) { imgui.tree(label, function() { // Show node summary _render_node_summary(imgui, node) - + // Recurse children - for (var i = 0; i < length(node.children); i++) { + var i = 0 + for (i = 0; i < length(node.children); i++) { _render_node_tree(imgui, node.children[i], depth + 1) } }) @@ -154,16 +155,18 @@ function _render_node_summary(imgui, node) { push(info, "img:" + node.image) } + var t = null if (node.text) { - var t = node.text + t = node.text if (length(t) > 20) t = text(t, 0, 17) + "..." push(info, "\"" + t + "\"") } + var fx = [] + var j = 0 if (node.effects && length(node.effects) > 0) { - var fx = [] - for (var i = 0; i < length(node.effects); i++) { - push(fx, node.effects[i].type) + for (j = 0; j < length(node.effects); j++) { + push(fx, node.effects[j].type) } push(info, "fx:[" + text(fx, ",") + "]") } @@ -189,9 +192,10 @@ function _render_node_inspector(imgui, node) { imgui.text("---") // Position + var pos = null if (node.pos) { imgui.text("Position") - var pos = imgui.slider("X", node.pos.x, -1000, 1000) + pos = imgui.slider("X", node.pos.x, -1000, 1000) if (pos != node.pos.x) node.pos.x = pos pos = imgui.slider("Y", node.pos.y, -1000, 1000) if (pos != node.pos.y) node.pos.y = pos @@ -242,15 +246,17 @@ function _render_node_inspector(imgui, node) { } // Effects + var ei = 0 + var fx = null if (node.effects && length(node.effects) > 0) { imgui.text("---") imgui.text("Effects:") - for (var i = 0; i < length(node.effects); i++) { - var fx = node.effects[i] + for (ei = 0; ei < length(node.effects); ei++) { + fx = node.effects[ei] imgui.tree(fx.type, function() { arrfor(array(fx), k => { + var v = fx[k] if (k != 'type' && k != 'source') { - var v = fx[k] if (is_number(v)) { fx[k] = imgui.slider(k, v, 0, 10) } else { @@ -288,21 +294,25 @@ function _render_graph_view(imgui, plan) { imgui.text("Persistent: " + text(length(array(plan.persistent_targets || {})))) imgui.text("---") - - for (var i = 0; i < length(plan.passes); i++) { - var pass = plan.passes[i] - var label = text(i) + ": " + pass.type - + + var i = 0 + var pass = null + var label = null + var target_info = null + for (i = 0; i < length(plan.passes); i++) { + pass = plan.passes[i] + label = text(i) + ": " + pass.type + if (pass.shader) label += " [" + pass.shader + "]" if (pass.renderer) label += " [" + pass.renderer + "]" - + if (imgui.button(label)) { _selected_pass = pass } - + // Show target info imgui.sameline(0) - var target_info = "" + target_info = "" if (pass.target) { if (pass.target == 'screen') { target_info = "-> screen" @@ -397,14 +407,17 @@ function _render_effects_panel(imgui) { imgui.text("Registered effects: " + text(length(effect_list))) imgui.text("---") - for (var i = 0; i < length(effect_list); i++) { - var name = effect_list[i] - var deff = effects_mod.get(name) - + var i = 0 + var name = null + var deff = null + for (i = 0; i < length(effect_list); i++) { + name = effect_list[i] + deff = effects_mod.get(name) + imgui.tree(name, function() { imgui.text("Type: " + (deff.type || 'unknown')) imgui.text("Requires target: " + (deff.requires_target ? "yes" : "no")) - + if (deff.params) { imgui.text("Parameters:") arrfor(array(deff.params), k => { diff --git a/ease.cm b/ease.cm index 2f999e48..cfdd6389 100644 --- a/ease.cm +++ b/ease.cm @@ -66,13 +66,19 @@ Ease.bounce = { out(t) { var n1 = 7.5625 var d1 = 2.75 - if (t < 1 / d1) { - return n1 * t * t - } else if (t < 2 / d1) { - return n1 * (t -= 1.5 / d1) * t + 0.75 - } else if (t < 2.5 / d1) { - return n1 * (t -= 2.25 / d1) * t + 0.9375 - } else return n1 * (t -= 2.625 / d1) * t + 0.984375 + var u = t + if (u < 1 / d1) { + return n1 * u * u + } else if (u < 2 / d1) { + u = u - 1.5 / d1 + return n1 * u * u + 0.75 + } else if (u < 2.5 / d1) { + u = u - 2.25 / d1 + return n1 * u * u + 0.9375 + } else { + u = u - 2.625 / d1 + return n1 * u * u + 0.984375 + } }, inout(t) { return t < 0.5 ? (1 - this.out(1 - 2 * t)) / 2 : (1 + this.out(2 * t - 1)) / 2 diff --git a/effects.cm b/effects.cm index 9e445881..26177636 100644 --- a/effects.cm +++ b/effects.cm @@ -53,8 +53,9 @@ effects.register('bloom', { var blur_src = thresh_target var texel = {x: 1 / size.width, y: 1 / size.height} var blur_count = params.blur_passes != null ? params.blur_passes : 3 + var i = 0 - for (var i = 0; i < blur_count; i++) { + for (i = 0; i < blur_count; i++) { push(passes, { type: 'shader', shader: 'blur', @@ -188,8 +189,9 @@ effects.register('blur', { var blur_b = ctx.alloc_target(size.width, size.height, 'blur_b') var src = input var blur_count = params.passes != null ? params.passes : 2 + var i = 0 - for (var i = 0; i < blur_count; i++) { + for (i = 0; i < blur_count; i++) { push(passes, { type: 'shader', shader: 'blur', diff --git a/emacs.cm b/emacs.cm index e613268d..81b977c2 100644 --- a/emacs.cm +++ b/emacs.cm @@ -38,41 +38,28 @@ action.on_input = function(action_id, action_data) emacs_notation += lower(key) } else { // Handle special keys - switch (key) { - case 'return': - case 'enter': - emacs_notation += "RET" - break - case 'space': - emacs_notation += "SPC" - break - case 'escape': - emacs_notation += "ESC" - break - case 'tab': - emacs_notation += "TAB" - break - case 'backspace': - emacs_notation += "DEL" - break - case 'delete': - emacs_notation += "delete" - break - case 'up': - emacs_notation += "up" - break - case 'down': - emacs_notation += "down" - break - case 'left': - emacs_notation += "left" - break - case 'right': - emacs_notation += "right" - break - default: - emacs_notation += key - break + if (key == 'return' || key == 'enter') { + emacs_notation += "RET" + } else if (key == 'space') { + emacs_notation += "SPC" + } else if (key == 'escape') { + emacs_notation += "ESC" + } else if (key == 'tab') { + emacs_notation += "TAB" + } else if (key == 'backspace') { + emacs_notation += "DEL" + } else if (key == 'delete') { + emacs_notation += "delete" + } else if (key == 'up') { + emacs_notation += "up" + } else if (key == 'down') { + emacs_notation += "down" + } else if (key == 'left') { + emacs_notation += "left" + } else if (key == 'right') { + emacs_notation += "right" + } else { + emacs_notation += key } } @@ -81,10 +68,11 @@ action.on_input = function(action_id, action_data) this.prefix_key = emacs_notation return } - + // If we have a prefix key, build the full command + var full_command = null if (this.prefix_key) { - var full_command = this.prefix_key + " " + emacs_notation + full_command = this.prefix_key + " " + emacs_notation this.prefix_key = null // Reset prefix key // scene.recurse(game.root, 'on_input', [full_command, action_data]) } else { diff --git a/examples/bunnymark/main.ce b/examples/bunnymark/main.ce index bb970948..ca05e33b 100644 --- a/examples/bunnymark/main.ce +++ b/examples/bunnymark/main.ce @@ -13,7 +13,8 @@ var bunnyTex = graphics.texture("bunny") var bunnies = [] // Start with some initial bunnies: -for (var i = 0; i < 100; i++) { +var i = 0; +for (i = 0; i < 100; i++) { push(bunnies, { x: random.random() * config.width, y: random.random() * config.height, @@ -25,8 +26,10 @@ for (var i = 0; i < 100; i++) { this.update = function(dt) { // If left mouse is down, spawn some more bunnies: var mouse = input.mousestate() + var i = 0; + var b = null; if (mouse.left) - for (var i = 0; i < 50; i++) { + for (i = 0; i < 50; i++) { push(bunnies, { x: mouse.x, y: mouse.y, @@ -36,8 +39,8 @@ this.update = function(dt) { } // Update bunny positions and bounce them inside the screen: - for (var i = 0; i < length(bunnies); i++) { - var b = bunnies[i] + for (i = 0; i < length(bunnies); i++) { + b = bunnies[i] b.x += b.vx * dt b.y += b.vy * dt diff --git a/examples/chess/chess.ce b/examples/chess/chess.ce index 9c953bd5..76db1e2a 100644 --- a/examples/chess/chess.ce +++ b/examples/chess/chess.ce @@ -28,26 +28,21 @@ var isMyTurn = false; function updateTitle() { var title = "Misty Chess - "; - - switch(gameState) { - case 'waiting': - title += "Press S to start server or J to join"; - break; - case 'searching': - title += "Searching for server..."; - break; - case 'server_waiting': - title += "Waiting for player to join..."; - break; - case 'connected': - if (myColor) { - title += (mover.turn == myColor ? "Your turn (" + myColor + ")" : "Opponent's turn (" + mover.turn + ")"); - } else { - title += mover.turn + " turn"; - } - break; + + if (gameState == 'waiting') { + title += "Press S to start server or J to join"; + } else if (gameState == 'searching') { + title += "Searching for server..."; + } else if (gameState == 'server_waiting') { + title += "Waiting for player to join..."; + } else if (gameState == 'connected') { + if (myColor) { + title += (mover.turn == myColor ? "Your turn (" + myColor + ")" : "Opponent's turn (" + mover.turn + ")"); + } else { + title += mover.turn + " turn"; + } } - + log.console(title) } @@ -179,13 +174,19 @@ var opponentMouseColor = [1.0, 0.0, 0.0, 1.0]; // Red for opponent mouse /* ── draw one 8×8 chess board ──────────────────────────────────── */ function drawBoard() { - for (var y = 0; y < 8; ++y) - for (var x = 0; x < 8; ++x) { - var isMyHover = hoverPos && hoverPos[0] == x && hoverPos[1] == y; - var isOpponentHover = opponentMousePos && opponentMousePos[0] == x && opponentMousePos[1] == y; - var isValidMove = selectPos && holdingPiece && isValidMoveForTurn(selectPos, [x, y]); - - var color = ((x+y)&1) ? dark : light; + var y = 0; + var x = 0; + var isMyHover = null; + var isOpponentHover = null; + var isValidMove = null; + var color = null; + for (y = 0; y < 8; ++y) + for (x = 0; x < 8; ++x) { + isMyHover = hoverPos && hoverPos[0] == x && hoverPos[1] == y; + isOpponentHover = opponentMousePos && opponentMousePos[0] == x && opponentMousePos[1] == y; + isValidMove = selectPos && holdingPiece && isValidMoveForTurn(selectPos, [x, y]); + + color = ((x+y)&1) ? dark : light; if (isValidMove) { color = allowedColor; // Gold for allowed moves @@ -220,47 +221,51 @@ function isValidMoveForTurn(from, to) { /* ── draw every live piece ─────────────────────────────────────── */ function drawPieces() { - grid.each(function (piece) { - if (piece.captured) return; - + var piece = null; + var r = null; + var opponentPiece = null; + + grid.each(function (p) { + if (p.captured) return; + // Skip drawing the piece being held (by me or opponent) - if (holdingPiece && selectPos && - piece.coord[0] == selectPos[0] && - piece.coord[1] == selectPos[1]) { - return; - } - - // Skip drawing the piece being held by opponent - if (opponentHoldingPiece && opponentSelectPos && - piece.coord[0] == opponentSelectPos[0] && - piece.coord[1] == opponentSelectPos[1]) { + if (holdingPiece && selectPos && + p.coord[0] == selectPos[0] && + p.coord[1] == selectPos[1]) { return; } - var r = { x: piece.coord[0]*S, y: piece.coord[1]*S, + // Skip drawing the piece being held by opponent + if (opponentHoldingPiece && opponentSelectPos && + p.coord[0] == opponentSelectPos[0] && + p.coord[1] == opponentSelectPos[1]) { + return; + } + + var pr = { x: p.coord[0]*S, y: p.coord[1]*S, width:S, height:S }; - draw2d.image(piece.sprite, r); + draw2d.image(p.sprite, pr); }); - + // Draw the held piece at the mouse position if we're holding one if (holdingPiece && selectPos && hoverPos) { - var piece = grid.at(selectPos)[0]; + piece = grid.at(selectPos)[0]; if (piece) { - var r = { x: hoverPos[0]*S, y: hoverPos[1]*S, + r = { x: hoverPos[0]*S, y: hoverPos[1]*S, width:S, height:S }; - + draw2d.image(piece.sprite, r); } } - + // Draw opponent's held piece if they're dragging one if (opponentHoldingPiece && opponentSelectPos && opponentMousePos) { - var opponentPiece = grid.at(opponentSelectPos)[0]; + opponentPiece = grid.at(opponentSelectPos)[0]; if (opponentPiece) { - var r = { x: opponentMousePos[0]*S, y: opponentMousePos[1]*S, + r = { x: opponentMousePos[0]*S, y: opponentMousePos[1]*S, width:S, height:S }; - + // Draw with slight transparency to show it's the opponent's piece draw2d.image(opponentPiece.sprite, r); } @@ -325,13 +330,16 @@ function joinServer() { } $receiver(e => { + var fromCell = null; + var piece = null; + if (e.kind == 'update') send(e, update(e.dt)) else if (e.kind == 'draw') send(e, draw()) else if (e.type == 'game_start' || e.type == 'move' || e.type == 'greet') log.console("Receiver got message:", e.type, e); - + if (e.type == 'greet') { log.console("Server received greet from client"); // Store the client's actor object for ongoing communication @@ -339,7 +347,7 @@ $receiver(e => { log.console("Stored client actor:", opponent); gameState = 'connected'; updateTitle(); - + // Send game_start to the client log.console("Sending game_start to client"); send(opponent, { @@ -357,9 +365,9 @@ $receiver(e => { } else if (e.type == 'move') { log.console("Received move from opponent:", e.from, "to", e.to); // Apply opponent's move - var fromCell = grid.at(e.from); + fromCell = grid.at(e.from); if (length(fromCell)) { - var piece = fromCell[0]; + piece = fromCell[0]; if (mover.tryMove(piece, e.to)) { isMyTurn = true; // It's now our turn updateTitle(); diff --git a/examples/chess/grid.cm b/examples/chess/grid.cm index bd72fa48..3f0971db 100644 --- a/examples/chess/grid.cm +++ b/examples/chess/grid.cm @@ -2,11 +2,13 @@ function grid(w, h) { var newgrid = meme(grid_prototype) newgrid.width = w; newgrid.height = h; - // create a height×width array of empty lists + // create a height*width array of empty lists newgrid.cells = array(h); - for (var y = 0; y < h; y++) { + var y = 0; + var x = 0; + for (y = 0; y < h; y++) { newgrid.cells[y] = array(w); - for (var x = 0; x < w; x++) { + for (x = 0; x < w; x++) { newgrid.cells[y][x] = []; // each cell holds its own list } } @@ -45,9 +47,12 @@ var grid_prototype = { // call fn(entity, coord) for every entity in every cell each(fn) { - for (var y = 0; y < this.height; y++) { - for (var x = 0; x < this.width; x++) { - def list = this.cells[y][x] + var list = null; + var y = 0; + var x = 0; + for (y = 0; y < this.height; y++) { + for (x = 0; x < this.width; x++) { + list = this.cells[y][x] arrfor(list, function(entity) { fn(entity, entity.coord); }) @@ -57,9 +62,11 @@ var grid_prototype = { // printable representation toString() { - var out = `grid [${this.width}×${this.height}]\n`; - for (var y = 0; y < this.height; y++) { - for (var x = 0; x < this.width; x++) { + var out = `grid [${this.width}x${this.height}]\n`; + var y = 0; + var x = 0; + for (y = 0; y < this.height; y++) { + for (x = 0; x < this.width; x++) { out += 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 2ae9b5d7..e3a1d387 100644 --- a/examples/chess/movement.cm +++ b/examples/chess/movement.cm @@ -10,9 +10,9 @@ var MovementSystem_prototype = { tryMove: function (piece, to) { if (piece.colour != this.turn) return false; - // normalise ‘to’ into our hybrid coord - var dest = [to.x ?? t[0], - to.y ?? to[1]]; + // normalise 'to' into our hybrid coord + var dest = [!is_null(to.x) ? to.x : to[0], + !is_null(to.y) ? to.y : to[1]]; if (!this.grid.inBounds(dest)) return false; if (!this.rules.canMove(piece, piece.coord, dest, this.grid)) return false; diff --git a/examples/chess/pieces.cm b/examples/chess/pieces.cm index e79c4dea..ad8e60bb 100644 --- a/examples/chess/pieces.cm +++ b/examples/chess/pieces.cm @@ -10,7 +10,7 @@ function Piece(kind, colour) { } function startingPosition(grid) { - var W = 'white', B = 'black', x; + var W = 'white', B = 'black', x = 0; // pawns for (x = 0; x < 8; x++) { diff --git a/examples/chess/rules.cm b/examples/chess/rules.cm index f021df07..fa4b77f6 100644 --- a/examples/chess/rules.cm +++ b/examples/chess/rules.cm @@ -1,30 +1,30 @@ -/* helper – robust coord access */ -function cx(c) { return c.x ?? c[0] } -function cy(c) { return c.y ?? c[1] } +/* helper -- robust coord access */ +function cx(c) { return !is_null(c.x) ? c.x : c[0] } +function cy(c) { return !is_null(c.y) ? c.y : c[1] } /* simple move-shape checks */ var deltas = { - pawn: function (pc, dx, dy, grid, to) { + pawn: function (pc, dx, dy, ctx) { var dir = (pc.colour == 'white') ? -1 : 1; var base = (pc.colour == 'white') ? 6 : 1; - var one = (dy == dir && dx == 0 && length(grid.at(to)) == 0); + 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(grid.at({ x: cx(pc.coord), y: cy(pc.coord)+dir })) == 0 && - length(grid.at(to)) == 0); - var cap = (dy == dir && Math.abs(dx) == 1 && length(grid.at(to))); + length(ctx.grid.at({ x: cx(pc.coord), y: cy(pc.coord)+dir })) == 0 && + length(ctx.grid.at(ctx.to)) == 0); + var cap = (dy == dir && abs(dx) == 1 && length(ctx.grid.at(ctx.to))); return one || two || cap; }, rook : function (pc, dx, dy) { return (dx == 0 || dy == 0); }, - bishop: function (pc, dx, dy) { return Math.abs(dx) == Math.abs(dy); }, - queen : function (pc, dx, dy) { return (dx == 0 || dy == 0 || Math.abs(dx) == Math.abs(dy)); }, - knight: function (pc, dx, dy) { return (Math.abs(dx) == 1 && Math.abs(dy) == 2) || - (Math.abs(dx) == 2 && Math.abs(dy) == 1); }, - king : function (pc, dx, dy) { return Math.max(Math.abs(dx), Math.abs(dy)) == 1; } + bishop: function (pc, dx, dy) { return abs(dx) == abs(dy); }, + queen : function (pc, dx, dy) { return (dx == 0 || dy == 0 || abs(dx) == abs(dy)); }, + knight: function (pc, dx, dy) { return (abs(dx) == 1 && abs(dy) == 2) || + (abs(dx) == 2 && abs(dy) == 1); }, + king : function (pc, dx, dy) { return max(abs(dx), abs(dy)) == 1; } }; function clearLine(from, to, grid) { - var dx = Math.sign(cx(to) - cx(from)); - var dy = Math.sign(cy(to) - cy(from)); + var dx = sign(cx(to) - cx(from)); + var dy = sign(cy(to) - cy(from)); var x = cx(from) + dx, y = cy(from) + dy; while (x != cx(to) || y != cy(to)) { if (length(grid.at({ x: x, y: y }))) return false; @@ -37,7 +37,7 @@ function canMove(piece, from, to, grid) { var dx = cx(to) - cx(from); var dy = cy(to) - cy(from); var f = deltas[piece.kind]; - if (!f || !f(piece, dx, dy, grid, to)) return false; + if (!f || !f(piece, dx, dy, {grid: grid, to: to})) return false; if (piece.kind == 'knight') return true; return clearLine(from, to, grid); } diff --git a/examples/snake/main.ce b/examples/snake/main.ce index 09e32d9f..2b67860e 100644 --- a/examples/snake/main.ce +++ b/examples/snake/main.ce @@ -13,7 +13,7 @@ var cellSize = 20 var gridW = floor(config.width / cellSize) var gridH = floor(config.height / cellSize) -var snake, direction, nextDirection, apple +var snake = null, direction = null, nextDirection = null, apple = null var moveInterval = 0.1 var moveTimer = 0 var gameState = "playing" @@ -36,7 +36,8 @@ function resetGame() { function spawnApple() { apple = {x:floor(random.random()*gridW), y:floor(random.random()*gridH)} // Re-spawn if apple lands on snake - for (var i=0; i=COLS || y<0 || y>=ROWS) return true if (y>=0 && board[y][x]) return true } @@ -95,9 +101,12 @@ function collides(px, py, blocks) { // Lock piece into board function lockPiece() { - for (var i=0; i=0) board[y][x] = piece.color } } @@ -105,9 +114,12 @@ function lockPiece() { // Rotate 90° clockwise function rotate(blocks) { // (x,y) => (y,-x) - for (var i=0; i=0;) { + var r = ROWS-1; + var newRow = null; + var c = 0; + for (r=ROWS-1; r>=0;) { if (every(board[r], cell => cell)) { lines++ // remove row board = array(array(board, 0, r), array(board, r+1)) // add empty row on top - var newRow = [] - for (var c=0; c [b[0], b[1]]) + test = array(piece.blocks, b => [b[0], b[1]]) rotate(test) if (!collides(pieceX, pieceY, test)) piece.blocks = test } @@ -232,9 +248,19 @@ this.hud = function() { draw.rectangle({x:0, y:0, width:config.width, height:config.height}, [0,0,0,1]) // Draw board - for (var r=0; r box_aspect) { // Image wider than box - constrain by width scale = target_w / tex_w @@ -79,24 +79,35 @@ function _resolve_sprite_fit(sprite) { return sprite } + var cover_scale = null + var fit_ax = null + var fit_ay = null + var scale_w = null + var scale_h = null + var visible_w = null + var visible_h = null + var uv_w = null + var uv_h = null + var uv_x = null + var uv_y = null if (fit == 'cover') { // Fill box, preserve aspect (crop via UV) - var fit_ax = sprite.fit_anchor_x != null ? sprite.fit_anchor_x : 0.5 - var fit_ay = sprite.fit_anchor_y != null ? sprite.fit_anchor_y : 0.5 - - var scale_w = target_w / tex_w - var scale_h = target_h / tex_h - var scale = max(scale_w, scale_h) + fit_ax = sprite.fit_anchor_x != null ? sprite.fit_anchor_x : 0.5 + fit_ay = sprite.fit_anchor_y != null ? sprite.fit_anchor_y : 0.5 + + scale_w = target_w / tex_w + scale_h = target_h / tex_h + cover_scale = max(scale_w, scale_h) // Compute visible portion of texture in UV space - var visible_w = target_w / scale - var visible_h = target_h / scale - + visible_w = target_w / cover_scale + visible_h = target_h / cover_scale + // UV rect (0-1 space) - var uv_w = visible_w / tex_w - var uv_h = visible_h / tex_h - var uv_x = (1 - uv_w) * fit_ax - var uv_y = (1 - uv_h) * fit_ay + uv_w = visible_w / tex_w + uv_h = visible_h / tex_h + uv_x = (1 - uv_w) * fit_ax + uv_y = (1 - uv_h) * fit_ay sprite.width = target_w sprite.height = target_h @@ -122,12 +133,14 @@ film2d.register = function(drawable) { // Index by groups (effect routing only) var groups = drawable.groups || [] - for (var i = 0; i < length(groups); i++) { - var g = groups[i] + var i = 0 + var g = null + for (i = 0; i < length(groups); i++) { + g = groups[i] if (!group_index[g]) group_index[g] = [] push(group_index[g], id) } - + return id } @@ -135,21 +148,24 @@ film2d.unregister = function(id) { var id_str = text(id) var drawable = registry[id_str] if (!drawable) return - + // Remove from plane index var plane = drawable.plane || 'default' + var idx = null if (plane_index[plane]) { - var idx = find(plane_index[plane], id_str) + idx = find(plane_index[plane], id_str) if (idx != null) plane_index[plane] = array(array(plane_index[plane], 0, idx), array(plane_index[plane], idx+1)) } - + // Remove from group indices var groups = drawable.groups || [] - for (var i = 0; i < length(groups); i++) { - var g = groups[i] + var i = 0 + var g = null + for (i = 0; i < length(groups); i++) { + g = groups[i] if (group_index[g]) { - var idx = find(group_index[g], id_str) + idx = find(group_index[g], id_str) if (idx != null) group_index[g] = array(array(group_index[g], 0, idx), array(group_index[g], idx+1)) } @@ -172,9 +188,10 @@ film2d.unindex_group = function(id, group) { } film2d.reindex = function(id, old_groups, new_groups) { - for (var i = 0; i < length(old_groups); i++) + var i = 0 + for (i = 0; i < length(old_groups); i++) film2d.unindex_group(id, old_groups[i]) - for (var i = 0; i < length(new_groups); i++) + for (i = 0; i < length(new_groups); i++) film2d.index_group(id, new_groups[i]) } @@ -185,16 +202,22 @@ film2d.get = function(id) { // Query by plane and/or group - returns array of drawables film2d.query = function(selector) { var result = [] - + var ids = null + var i = 0 + var d = null + var groups = null + var seen = null + var g = 0 + // Query by plane (primary selection) if (selector.plane) { - var ids = plane_index[selector.plane] || [] - for (var i = 0; i < length(ids); i++) { - var d = registry[ids[i]] + ids = plane_index[selector.plane] || [] + for (i = 0; i < length(ids); i++) { + d = registry[ids[i]] if (d && d.visible != false) { // If also filtering by group, check membership if (selector.group) { - var groups = d.groups || [] + groups = d.groups || [] if (search(groups, selector.group) != null) push(result, d) } else { push(result, d) @@ -203,32 +226,32 @@ film2d.query = function(selector) { } return result } - + // Query by group only (for effect routing) if (selector.group) { - var ids = group_index[selector.group] || [] - for (var i = 0; i < length(ids); i++) { - var d = registry[ids[i]] + ids = group_index[selector.group] || [] + for (i = 0; i < length(ids); i++) { + d = registry[ids[i]] if (d && d.visible != false) push(result, d) } return result } - + if (selector.groups) { - var seen = {} - for (var g = 0; g < length(selector.groups); g++) { - var ids = group_index[selector.groups[g]] || [] - for (var i = 0; i < length(ids); i++) { + seen = {} + for (g = 0; g < length(selector.groups); g++) { + ids = group_index[selector.groups[g]] || [] + for (i = 0; i < length(ids); i++) { if (!seen[ids[i]]) { seen[ids[i]] = true - var d = registry[ids[i]] + d = registry[ids[i]] if (d && d.visible != false) push(result, d) } } } return result } - + // All drawables var draws = array(registry, id => registry[id]) result = array(result, filter(draws, d => d.visible != false)) @@ -277,10 +300,14 @@ film2d.render = function(params, render_backend) { // Bucket drawables by layer var buckets = {} - for (var i = 0; i < length(drawables); i++) { - var d = drawables[i] - var layer_key = text(d.layer) - var b = buckets[layer_key] + var i = 0 + var d = null + var layer_key = null + var b = null + for (i = 0; i < length(drawables); i++) { + d = drawables[i] + layer_key = text(d.layer) + b = buckets[layer_key] if (!b) { b = [] buckets[layer_key] = b @@ -296,19 +323,23 @@ film2d.render = function(params, render_backend) { // Merge buckets, y-sorting buckets that request it var y_down = camera && camera.y_down == true var sorted_drawables = [] + var li = 0 + var mode = null + var keys = null + var j = 0 - for (var li = 0; li < length(layers); li++) { - var layer_key = layers[li] - var b = buckets[layer_key] + for (li = 0; li < length(layers); li++) { + layer_key = layers[li] + b = buckets[layer_key] - var mode = layer_sort[layer_key] || "explicit" + mode = layer_sort[layer_key] || "explicit" if (mode == "y") { - var keys = array(b, d => _y_sort_key(d)) + keys = array(b, d => _y_sort_key(d)) b = sort(b, keys) // ascending feet-y if (!y_down) b = reverse(b) // y_up => smaller y draws later => reverse } - for (var j = 0; j < length(b); j++) push(sorted_drawables, b[j]) + for (j = 0; j < length(b); j++) push(sorted_drawables, b[j]) } drawables = sorted_drawables @@ -318,9 +349,10 @@ film2d.render = function(params, render_backend) { push(commands, { cmd: "set_camera", camera: camera }) var batches = _batch_drawables(drawables) + var batch = null - for (var i = 0; i < length(batches); i++) { - var batch = batches[i] + for (i = 0; i < length(batches); i++) { + batch = batches[i] if (batch.type == "sprite_batch") push(commands, { cmd: "draw_batch", batch_type: "sprites", geometry: { sprites: batch.sprites }, texture: batch.texture, material: batch.material }) else if (batch.type == "mesh2d_batch") @@ -341,17 +373,40 @@ function _batch_drawables(drawables) { var batches = [] var current = null var default_mat = {blend: 'alpha', sampler: 'nearest'} - - for (var i = 0; i < length(drawables); i++) { - var d = drawables[i] - + var i = 0 + var d = null + var tex = null + var mat = null + var particles = null + var emitter_opacity = 0 + var emitter_tint = null + var p = 0 + var part = null + var pc = null + var sprite = null + var tiles = null + var tile_w = 0 + var tile_h = 0 + var off_x = 0 + var off_y = 0 + var tilemap_opacity = 0 + var tilemap_tint = null + var x = 0 + var y = 0 + var img = null + var wx = 0 + var wy = 0 + + for (i = 0; i < length(drawables); i++) { + d = drawables[i] + if (d.type == 'sprite') { // Resolve fit mode (computes final width/height/uv_rect) _resolve_sprite_fit(d) - - var tex = d.texture || d.image - var mat = d.material || {blend: 'alpha', sampler: d.filter || 'nearest'} - + + tex = d.texture || d.image + mat = d.material || {blend: 'alpha', sampler: d.filter || 'nearest'} + if (current && current.type == 'sprite_batch' && current.texture == tex && _mat_eq(current.material, mat)) { push(current.sprites, d) } else { @@ -360,16 +415,16 @@ function _batch_drawables(drawables) { } } else if (d.type == 'particles') { // Convert particles to sprites - var tex = d.texture || d.image - var mat = d.material || default_mat - var particles = d.particles || [] - var emitter_opacity = d.opacity != null ? d.opacity : 1 - var emitter_tint = d.tint || {r: 1, g: 1, b: 1, a: 1} - - for (var p = 0; p < length(particles); p++) { - var part = particles[p] - var pc = part.color || {r: 1, g: 1, b: 1, a: 1} - var sprite = { + tex = d.texture || d.image + mat = d.material || default_mat + particles = d.particles || [] + emitter_opacity = d.opacity != null ? d.opacity : 1 + emitter_tint = d.tint || {r: 1, g: 1, b: 1, a: 1} + + for (p = 0; p < length(particles); p++) { + part = particles[p] + pc = part.color || {r: 1, g: 1, b: 1, a: 1} + sprite = { type: 'sprite', pos: part.pos, width: (d.width || 16) * (part.scale || 1), @@ -380,7 +435,7 @@ function _batch_drawables(drawables) { opacity: emitter_opacity, tint: emitter_tint } - + if (current && current.type == 'sprite_batch' && current.texture == tex && _mat_eq(current.material, mat)) { push(current.sprites, sprite) } else { @@ -390,25 +445,25 @@ function _batch_drawables(drawables) { } } else if (d.type == 'tilemap') { // Expand tilemap to sprites - var tiles = d.tiles || [] - var tile_w = d.tile_width || 1 - var tile_h = d.tile_height || 1 - var off_x = d.offset_x || 0 - var off_y = d.offset_y || 0 - var tilemap_opacity = d.opacity != null ? d.opacity : 1 - var tilemap_tint = d.tint || {r: 1, g: 1, b: 1, a: 1} - - for (var x = 0; x < length(tiles); x++) { + tiles = d.tiles || [] + tile_w = d.tile_width || 1 + tile_h = d.tile_height || 1 + off_x = d.offset_x || 0 + off_y = d.offset_y || 0 + tilemap_opacity = d.opacity != null ? d.opacity : 1 + tilemap_tint = d.tint || {r: 1, g: 1, b: 1, a: 1} + + for (x = 0; x < length(tiles); x++) { if (!tiles[x]) continue - for (var y = 0; y < length(tiles[x]); y++) { - var img = tiles[x][y] + for (y = 0; y < length(tiles[x]); y++) { + img = tiles[x][y] if (!img) continue - - var wx = (x + off_x) * tile_w - var wy = (y + off_y) * tile_h - + + wx = (x + off_x) * tile_w + wy = (y + off_y) * tile_h + // Center anchor for sprite - var sprite = { + sprite = { type: 'sprite', image: img, pos: {x: wx + tile_w/2, y: wy + tile_h/2}, @@ -420,10 +475,10 @@ function _batch_drawables(drawables) { opacity: tilemap_opacity, tint: tilemap_tint } - + // Batching - var tex = img - var mat = default_mat + tex = img + mat = default_mat if (current && current.type == 'sprite_batch' && current.texture == tex && _mat_eq(current.material, mat)) { push(current.sprites, sprite) } else { @@ -434,8 +489,8 @@ function _batch_drawables(drawables) { } } else if (d.type == 'mesh2d') { // Mesh2d drawables - arbitrary triangle meshes (for lines, ropes, etc) - var tex = d.texture || d.image - var mat = d.material || {blend: d.blend || 'alpha', sampler: d.filter || 'linear'} + tex = d.texture || d.image + mat = d.material || {blend: d.blend || 'alpha', sampler: d.filter || 'linear'} if (current && current.type == 'mesh2d_batch' && current.texture == tex && _mat_eq(current.material, mat)) { push(current.meshes, d) diff --git a/fx_graph.cm b/fx_graph.cm index c87584fb..760caead 100644 --- a/fx_graph.cm +++ b/fx_graph.cm @@ -60,11 +60,11 @@ var fx_graph = {} fx_graph.add_node = function(type, params) { - params = params || {} + var local_params = params || {} var node = { id: this.next_id++, type: type, - params: params, + params: local_params, output: {node_id: this.next_id - 1, slot: 'output'} } push(this.nodes, node) @@ -137,7 +137,7 @@ NODE_EXECUTORS.render_view = function(params, backend) { var renderer = params.renderer // Determine target - var target + var target = null if (target_spec == 'screen') { target = 'screen' } else if (target_spec && target_spec.texture) { @@ -257,17 +257,18 @@ NODE_EXECUTORS.clip_rect = function(params, backend) { // Insert scissor after begin_render var insert_idx = 0 - for (var i = 0; i < length(commands); i++) { + var i = 0 + for (i = 0; i < length(commands); i++) { if (commands[i].cmd == 'begin_render') { insert_idx = i + 1 break } } - + commands = array(array(array(commands, 0, insert_idx), [{cmd: 'scissor', rect: rect}]), array(commands, insert_idx)) - + // Add scissor reset before end_render - for (var i = length(commands) - 1; i >= 0; i--) { + for (i = length(commands) - 1; i >= 0; i--) { if (commands[i].cmd == 'end_render') { commands = array(array(array(commands, 0, i), [{cmd: 'scissor', rect:null}]) ,array(commands, i+1)) break @@ -287,7 +288,8 @@ NODE_EXECUTORS.blit = function(params, backend) { var src_target = input && input.target ? input.target : input if (!src_target) return {target: null, commands: []} - var target + var target = null + var key = null if (target_spec == 'screen') { target = 'screen' } else if (target_spec && target_spec.target) { @@ -298,7 +300,7 @@ NODE_EXECUTORS.blit = function(params, backend) { target = target_spec } else if (target_spec && target_spec.width) { // Target spec - use a consistent key based on the spec itself - var key = `blit_${target_spec.width}x${target_spec.height}` + key = `blit_${target_spec.width}x${target_spec.height}` target = backend.get_or_create_target(target_spec.width, target_spec.height, key) } else { return {target: null, commands: []} @@ -336,16 +338,18 @@ NODE_EXECUTORS.shader_pass = function(params, backend) { if (!input || !input.target) return {target: null, commands: []} var src = input.target - var target - + var target = null + var w = 0 + var h = 0 + if (output_spec == 'screen') { target = 'screen' } else if (output_spec && output_spec.texture) { target = output_spec } else { // Default to input size if not specified - var w = output_spec && output_spec.width ? output_spec.width : src.width - var h = output_spec && output_spec.height ? output_spec.height : src.height + w = output_spec && output_spec.width ? output_spec.width : src.width + h = output_spec && output_spec.height ? output_spec.height : src.height target = backend.get_or_create_target(w, h, 'shader_' + shader + '_' + params._node_id) } diff --git a/geometry.c b/geometry.c index 09450195..4aabbdd1 100644 --- a/geometry.c +++ b/geometry.c @@ -615,7 +615,7 @@ JSC_CCALL(geometry_tilemap_to_data, JS_FreeValue(js, pos_y_val); JSValue tiles_array = JS_GetPropertyStr(js, tilemap_obj, "tiles"); - if (!JS_IsArray(js, tiles_array)) { + if (!JS_IsArray(tiles_array)) { JS_FreeValue(js, tiles_array); return JS_ThrowTypeError(js, "tilemap.tiles must be an array"); } @@ -624,11 +624,11 @@ JSC_CCALL(geometry_tilemap_to_data, int tile_count = 0; int tiles_len = JS_ArrayLength(js, tiles_array); for (int x = 0; x < tiles_len; x++) { - JSValue col = JS_GetPropertyUint32(js, tiles_array, x); - if (JS_IsArray(js, col)) { + JSValue col = JS_GetPropertyNumber(js, tiles_array, x); + if (JS_IsArray(col)) { int col_len = JS_ArrayLength(js, col); for (int y = 0; y < col_len; y++) { - JSValue tile = JS_GetPropertyUint32(js, col, y); + JSValue tile = JS_GetPropertyNumber(js, col, y); if (!JS_IsNull(tile) && !JS_IsNull(tile)) { tile_count++; } @@ -657,11 +657,11 @@ JSC_CCALL(geometry_tilemap_to_data, int index_idx = 0; for (int x = 0; x < tiles_len; x++) { - JSValue col = JS_GetPropertyUint32(js, tiles_array, x); - if (JS_IsArray(js, col)) { + JSValue col = JS_GetPropertyNumber(js, tiles_array, x); + if (JS_IsArray(col)) { int col_len = JS_ArrayLength(js, col); for (int y = 0; y < col_len; y++) { - JSValue tile = JS_GetPropertyUint32(js, col, y); + JSValue tile = JS_GetPropertyNumber(js, col, y); if (!JS_IsNull(tile) && !JS_IsNull(tile)) { // Calculate world position // x and y are array indices, need to convert to logical coordinates @@ -781,7 +781,7 @@ static void print_buffers(float *xy_data, float *uv_data, SDL_FColor *color_data JSC_CCALL(geometry_sprites_to_data, JSValue sprites_array = argv[0]; - if (!JS_IsArray(js, sprites_array)) { + if (!JS_IsArray(sprites_array)) { return JS_ThrowTypeError(js, "sprites must be an array"); } @@ -804,7 +804,7 @@ JSC_CCALL(geometry_sprites_to_data, int index_idx = 0; for (int i = 0; i < sprite_count; i++) { - JSValue sprite = JS_GetPropertyUint32(js, sprites_array, i); + JSValue sprite = JS_GetPropertyNumber(js, sprites_array, i); // Get sprite properties JSValue pos_val = JS_GetPropertyStr(js, sprite, "pos"); @@ -989,16 +989,16 @@ JSC_CCALL(geometry_transform_xy_blob, } JSValue camera_params = argv[1]; - if (!JS_IsArray(js, camera_params) || JS_ArrayLength(js, camera_params) != 4) { + if (!JS_IsArray(camera_params) || JS_ArrayLength(js, camera_params) != 4) { return JS_ThrowTypeError(js, "Second argument must be an array of 4 camera transform parameters [a, c, e, f]"); } // Get camera transform parameters double a, c, e, f; - JSValue a_val = JS_GetPropertyUint32(js, camera_params, 0); - JSValue c_val = JS_GetPropertyUint32(js, camera_params, 1); - JSValue e_val = JS_GetPropertyUint32(js, camera_params, 2); - JSValue f_val = JS_GetPropertyUint32(js, camera_params, 3); + JSValue a_val = JS_GetPropertyNumber(js, camera_params, 0); + JSValue c_val = JS_GetPropertyNumber(js, camera_params, 1); + JSValue e_val = JS_GetPropertyNumber(js, camera_params, 2); + JSValue f_val = JS_GetPropertyNumber(js, camera_params, 3); JS_ToFloat64(js, &a, a_val); JS_ToFloat64(js, &c, c_val); @@ -1058,7 +1058,7 @@ JSC_CCALL(geometry_array_blob, size_t len = JS_ArrayLength(js,arr); float data[len]; for (int i = 0; i < len; i++) { - JSValue val = JS_GetPropertyUint32(js,arr,i); + JSValue val = JS_GetPropertyNumber(js,arr,i); data[i] = js2number(js, val); JS_FreeValue(js,val); } @@ -1168,7 +1168,7 @@ JSC_CCALL(geometry_weave, int num_elements = -1; for (uint32_t i = 0; i < stream_count; i++) { - JSValue stream_obj = JS_GetPropertyUint32(js, argv[0], i); + JSValue stream_obj = JS_GetPropertyNumber(js, argv[0], i); JSValue data_blob = JS_GetPropertyStr(js, stream_obj, "data"); JSValue stride_val = JS_GetPropertyStr(js, stream_obj, "stride"); diff --git a/gestures.cm b/gestures.cm index 1c431d03..7ba1bb3e 100644 --- a/gestures.cm +++ b/gestures.cm @@ -17,11 +17,23 @@ gesture.reset = function() { gesture.on_input = function(action_id, action) { if (search(action_id, 'gamepad_touchpad_') == null) return - + var finger = action.finger || 0 var touchpad = action.touchpad || 0 var fingerId = `${touchpad}_${finger}` - + var touchCount = 0 + var fingers = null + var currentDist = 0 + var d = 0 + var gesture_type = null + var touch = null + var dt = 0 + var dx = 0 + var dy = 0 + var absX = 0 + var absY = 0 + var dir = null + if (action_id == 'gamepad_touchpad_down') { // Add new touch this.touches[fingerId] = { @@ -31,9 +43,9 @@ gesture.on_input = function(action_id, action) { startY: action.y, startTime: time.number() } - - var touchCount = length(array(this.touches)) - + + touchCount = length(array(this.touches)) + if (touchCount == 1) { // Single touch started this.gestureState = 'single' @@ -41,7 +53,7 @@ gesture.on_input = function(action_id, action) { } else if (touchCount == 2) { // Two touches - potential pinch this.gestureState = 'multi' - var fingers = array(array(this.touches), k => this.touches[k]) + fingers = array(array(this.touches), k => this.touches[k]) this.startDist = this.dist(fingers[0], fingers[1]) } } @@ -50,17 +62,17 @@ gesture.on_input = function(action_id, action) { // Update touch position this.touches[fingerId].x = action.x this.touches[fingerId].y = action.y - - var touchCount = length(array(this.touches)) - + + touchCount = length(array(this.touches)) + if (touchCount == 2 && this.gestureState == 'multi') { // Check for pinch gesture - var fingers = array(array(this.touches), k => this.touches[k]) - var currentDist = this.dist(fingers[0], fingers[1]) - var d = currentDist - this.startDist - + fingers = array(array(this.touches), k => this.touches[k]) + currentDist = this.dist(fingers[0], fingers[1]) + d = currentDist - this.startDist + if (abs(d) >= this.PINCH_TH) { - var gesture_type = d > 0 ? 'pinch_out' : 'pinch_in' + gesture_type = d > 0 ? 'pinch_out' : 'pinch_in' // scene.recurse(game.root, 'on_input', [gesture_type, { delta: d }]) this.startDist = currentDist } @@ -69,19 +81,20 @@ gesture.on_input = function(action_id, action) { } else if (action_id == 'gamepad_touchpad_up') { if (this.touches[fingerId]) { - var touch = this.touches[fingerId] - var touchCount = length(array(this.touches)) - + touch = this.touches[fingerId] + touchCount = length(array(this.touches)) + // Check for swipe if this was the only/last touch if (touchCount == 1 && this.gestureState == 'single') { - var dt = time.number() - touch.startTime - var dx = action.x - touch.startX - var dy = action.y - touch.startY - + dt = time.number() - touch.startTime + dx = action.x - touch.startX + dy = action.y - touch.startY + if (dt < this.MAX_TIME / 1000) { // Convert to seconds - var absX = abs(dx), absY = abs(dy) + absX = abs(dx) + absY = abs(dy) if (absX > this.MIN_SWIPE / 100 || absY > this.MIN_SWIPE / 100) { // Normalize for 0-1 range - var dir = absX > absY + dir = absX > absY ? (dx > 0 ? 'swipe_right' : 'swipe_left') : (dy > 0 ? 'swipe_down' : 'swipe_up') audio.play('swipe') @@ -89,10 +102,10 @@ gesture.on_input = function(action_id, action) { } } } - + // Remove touch delete this.touches[fingerId] - + // Reset if no touches left if (length(array(this.touches)) == 0) { this.gestureState = null diff --git a/graphics.cm b/graphics.cm index d718c520..f9776d24 100644 --- a/graphics.cm +++ b/graphics.cm @@ -42,7 +42,7 @@ function calc_image_size(img) { function decorate_rect_px(img) { // default UV rect is the whole image if none supplied - img.rect ??= {x:0, y:0, width:1, height:1} // [u0,v0,uw,vh] in 0-1 + if (!img.rect) img.rect = {x:0, y:0, width:1, height:1} // [u0,v0,uw,vh] in 0-1 var width = 0, height = 0; if (img.texture) { @@ -87,27 +87,32 @@ function wrapFrames(arr){ /* [{surface,time,rect}, …] → [{image,t } }); } -function makeAnim(frames, loop=true){ - return { frames, loop } +function makeAnim(frames, loop){ + var local_loop = loop != null ? loop : true + return { frames: frames, loop: local_loop } } function decode_image(bytes, ext) { - switch(ext) { - case 'gif': return decode_gif(gif.decode(bytes)) - case 'ase': - case 'aseprite': return decode_aseprite(aseprite.decode(bytes)) - case 'qoi': return qoi.decode(bytes) // returns single surface - case 'png': return png.decode(bytes) // returns single surface - case 'jpg': - case 'jpeg': return png.decode(bytes) // png.decode handles jpg too via stb_image - case 'bmp': return png.decode(bytes) // png.decode handles bmp too via stb_image - default: - // Try QOI first since it's fast to check - var qoi_result = qoi.decode(bytes) - if (qoi_result) return qoi_result - // Fall back to png decoder for other formats (uses stb_image) - return png.decode(bytes) + var qoi_result = null + if (ext == 'gif') { + return decode_gif(gif.decode(bytes)) + } else if (ext == 'ase' || ext == 'aseprite') { + return decode_aseprite(aseprite.decode(bytes)) + } else if (ext == 'qoi') { + return qoi.decode(bytes) // returns single surface + } else if (ext == 'png') { + return png.decode(bytes) // returns single surface + } else if (ext == 'jpg' || ext == 'jpeg') { + return png.decode(bytes) // png.decode handles jpg too via stb_image + } else if (ext == 'bmp') { + return png.decode(bytes) // png.decode handles bmp too via stb_image + } else { + // Try QOI first since it's fast to check + qoi_result = qoi.decode(bytes) + if (qoi_result) return qoi_result + // Fall back to png decoder for other formats (uses stb_image) + return png.decode(bytes) } } @@ -151,52 +156,49 @@ function decode_aseprite(decoded) { } function create_image(path){ - try{ - def bytes = io.slurp(path); + def bytes = io.slurp(path); - var ext = pop(array(path, '.')) - var raw = decode_image(bytes, ext); - - /* ── Case A: single surface (from make_texture) ────────────── */ - if(raw && raw.width && raw.pixels && !is_array(raw)) { - return graphics.Image(raw) - } + var ext = pop(array(path, '.')) + var raw = decode_image(bytes, ext); + var anims = null + var keys = null - /* ── Case B: array of surfaces (from make_gif) ────────────── */ - if(is_array(raw)) { - // Single frame GIF returns array with one surface - if(length(raw) == 1 && !raw[0].time) { - return graphics.Image(raw[0]) - } - // Multiple frames - create animation - return makeAnim(wrapFrames(raw), true); - } - - if(is_object(raw) && !raw.width) { - if(raw.surface) - return graphics.Image(raw.surface) - - if(raw.frames && is_array(raw.frames) && raw.loop != null) - return makeAnim(wrapFrames(raw.frames), !!raw.loop); - - def anims = {}; - var keys = array(raw) - arrfor(keys, function(name) { - var anim = raw[name] - if(anim && is_array(anim.frames)) - anims[name] = makeAnim(wrapFrames(anim.frames), !!anim.loop); - else if(anim && anim.surface) - anims[name] = graphics.Image(anim.surface); - }) - if(length(array(anims))) return anims; - } - - throw Error('Unsupported image structure from decoder'); - - }catch(e){ - log.error(`Error loading image ${path}: ${e.message}`); - throw e; + /* ── Case A: single surface (from make_texture) ────────────── */ + if(raw && raw.width && raw.pixels && !is_array(raw)) { + return graphics.Image(raw) } + + /* ── Case B: array of surfaces (from make_gif) ────────────── */ + if(is_array(raw)) { + // Single frame GIF returns array with one surface + if(length(raw) == 1 && !raw[0].time) { + return graphics.Image(raw[0]) + } + // Multiple frames - create animation + return makeAnim(wrapFrames(raw), true); + } + + if(is_object(raw) && !raw.width) { + if(raw.surface) + return graphics.Image(raw.surface) + + if(raw.frames && is_array(raw.frames) && raw.loop != null) + return makeAnim(wrapFrames(raw.frames), !!raw.loop); + + anims = {}; + keys = array(raw) + arrfor(keys, function(name) { + var anim = raw[name] + if(anim && is_array(anim.frames)) + anims[name] = makeAnim(wrapFrames(anim.frames), !!anim.loop); + else if(anim && anim.surface) + anims[name] = graphics.Image(anim.surface); + }) + if(length(array(anims))) return anims; + } + + log.error(`Error loading image ${path}: Unsupported image structure from decoder`); + return null } var image = {} @@ -212,7 +214,7 @@ image.dimensions = function() { return [width, height].scale([this.rect[2], this.rect[3]]) } -var spritesheet +var spritesheet = null var sheet_frames = [] var sheetsize = 1024 @@ -249,8 +251,10 @@ graphics.from_surface = function(surf) graphics.from = function(id, data) { - if (!is_text(id)) - throw Error('Expected a string ID') + if (!is_text(id)) { + log.error('Expected a string ID') + return null + } if (is_blob(data)) return graphics.texture_from_data(data) @@ -258,14 +262,20 @@ graphics.from = function(id, data) graphics.texture = function texture(path) { if (is_proto(path, graphics.Image)) return path - - if (!is_text(path)) - throw Error('need a string for graphics.texture') + + if (!is_text(path)) { + log.error('need a string for graphics.texture') + return null + } var parts = array(path, ':') var id = parts[0] var animName = parts[1] var frameIndex = parts[2] + var ipath = null + var result = null + var idx = null + var anim = null // Handle the case where animName is actually a frame index (e.g., "gears:0") if (animName != null && frameIndex == null && !is_null(number(animName))) { @@ -274,14 +284,14 @@ graphics.texture = function texture(path) { } if (!cache[id]) { - var ipath = res.find_image(id) - + ipath = res.find_image(id) + if (!ipath) { // If still not found, return notex return graphics.texture('notex') } - - var result = create_image(ipath) + + result = create_image(ipath) cache[id] = result } @@ -294,7 +304,7 @@ graphics.texture = function texture(path) { if (!animName && frameIndex != null) { // If cached is a single animation (has .frames property) if (cached.frames && is_array(cached.frames)) { - var idx = number(frameIndex) + idx = number(frameIndex) if (idx == null) return cached // Wrap the index idx = idx % length(cached.frames) @@ -319,8 +329,8 @@ graphics.texture = function texture(path) { // If cached is a single animation (has .frames property) if (cached.frames && is_array(cached.frames)) { if (frameIndex != null) { - var idx = number(frameIndex) - if (isNaN(idx)) return cached + idx = number(frameIndex) + if (idx == null) return cached // Wrap the index idx = idx % length(cached.frames) return cached.frames[idx].image @@ -331,13 +341,14 @@ graphics.texture = function texture(path) { // If cached is an object of multiple animations if (is_object(cached) && !cached.frames) { - var anim = cached[animName] + anim = cached[animName] if (!anim) - throw Error(`animation ${animName} not found in ${id}`) + log.error(`animation ${animName} not found in ${id}`) + return null if (frameIndex != null) { - var idx = number(frameIndex) - if (isNaN(idx)) return anim + idx = number(frameIndex) + if (idx == null) return anim if (is_proto(anim, graphics.Image)) { // Single image animation - any frame index returns the image @@ -412,19 +423,25 @@ var datas = [] graphics.get_font = function get_font(path) { if (is_object(path)) return path - if (!is_text(path)) - throw Error(`Can't find font with path: ${path}`) + if (!is_text(path)) { + log.error(`Can't find font with path: ${path}`) + return null + } var parts = array(path, '.') var size = 16 // default size + var font_path = path parts[1] = number(parts[1]) if (parts[1]) { - path = parts[0] + font_path = parts[0] size = parts[1] } - var fullpath = res.find_font(path) - if (!fullpath) throw Error(`Cannot load font ${path}`) + var fullpath = res.find_font(font_path) + if (!fullpath) { + log.error(`Cannot load font ${path}`) + return null + } var fontstr = `${fullpath}.${size}` if (fontcache[fontstr]) return fontcache[fontstr] @@ -441,7 +458,8 @@ graphics.queue_sprite_mesh = function(queue) { var sprites = filter(queue, x => x.type == 'sprite') if (length(sprites) == 0) return [] var mesh = graphics.make_sprite_mesh(sprites) - for (var i = 0; i < length(sprites); i++) { + var i = 0 + for (i = 0; i < length(sprites); i++) { sprites[i].mesh = mesh sprites[i].first_index = i*6 sprites[i].num_indices = 6 diff --git a/imgui.cpp b/imgui.cpp index 5c275317..4223767d 100644 --- a/imgui.cpp +++ b/imgui.cpp @@ -38,8 +38,8 @@ static inline ImVec2 js2imvec2(JSContext *js, JSValue v) { ImVec2 c; double dx, dy; - JSValue x = JS_GetPropertyUint32(js, v, 0); - JSValue y = JS_GetPropertyUint32(js, v, 1); + JSValue x = JS_GetPropertyNumber(js, v, 0); + JSValue y = JS_GetPropertyNumber(js, v, 1); JS_ToFloat64(js, &dx, x); JS_ToFloat64(js, &dy, y); JS_FreeValue(js, x); @@ -53,10 +53,10 @@ static inline ImVec4 js2imvec4(JSContext *js, JSValue v) { ImVec4 c; double dx, dy, dz, dw; - JSValue x = JS_GetPropertyUint32(js, v, 0); - JSValue y = JS_GetPropertyUint32(js, v, 1); - JSValue z = JS_GetPropertyUint32(js, v, 2); - JSValue w = JS_GetPropertyUint32(js, v, 3); + JSValue x = JS_GetPropertyNumber(js, v, 0); + JSValue y = JS_GetPropertyNumber(js, v, 1); + JSValue z = JS_GetPropertyNumber(js, v, 2); + JSValue w = JS_GetPropertyNumber(js, v, 3); JS_ToFloat64(js, &dx, x); JS_ToFloat64(js, &dy, y); JS_ToFloat64(js, &dz, z); @@ -75,8 +75,8 @@ static inline ImVec4 js2imvec4(JSContext *js, JSValue v) static inline JSValue imvec22js(JSContext *js, ImVec2 vec) { JSValue v = JS_NewObject(js); - JS_SetPropertyUint32(js, v, 0, JS_NewFloat64(js, vec.x)); - JS_SetPropertyUint32(js, v, 1, JS_NewFloat64(js, vec.y)); + JS_SetPropertyNumber(js, v, 0, JS_NewFloat64(js, vec.x)); + JS_SetPropertyNumber(js, v, 1, JS_NewFloat64(js, vec.y)); return v; } @@ -169,17 +169,17 @@ JSC_SCALL(imgui_combo, if (JS_IsNumber(argv[1])) { current_item = js2number(js, argv[1]); - } else if (JS_IsString(argv[1])) { + } else if (JS_IsText(argv[1])) { preview_str = JS_ToCString(js, argv[1]); } - if (JS_IsArray(js, argv[2])) { + if (JS_IsArray(argv[2])) { // Handle array of strings int item_count = JS_ArrayLength(js, argv[2]); const char **items = (const char**)malloc(sizeof(char*) * item_count); for (int i = 0; i < item_count; i++) { - JSValue item = JS_GetPropertyUint32(js, argv[2], i); + JSValue item = JS_GetPropertyNumber(js, argv[2], i); items[i] = JS_ToCString(js, item); JS_FreeValue(js, item); } @@ -210,7 +210,7 @@ JSC_SCALL(imgui_combo, } free(items); - } else if (JS_IsString(argv[2])) { + } else if (JS_IsText(argv[2])) { // Handle single string with \0 separators const char *items_str = JS_ToCString(js, argv[2]); @@ -248,13 +248,13 @@ JSC_SCALL(imgui_slider, float low = JS_IsNull(argv[2]) ? 0.0 : js2number(js, argv[2]); float high = JS_IsNull(argv[3]) ? 1.0 : js2number(js, argv[3]); - if (JS_IsArray(js, argv[1])) { + if (JS_IsArray(argv[1])) { int n = JS_ArrayLength(js, argv[1]); float a[4]; // Max 4 elements for SliderFloat4 // Read values from JS array for (int i = 0; i < n && i < 4; i++) { - JSValue val = JS_GetPropertyUint32(js, argv[1], i); + JSValue val = JS_GetPropertyNumber(js, argv[1], i); double d; JS_ToFloat64(js, &d, val); a[i] = (float)d; @@ -276,7 +276,7 @@ JSC_SCALL(imgui_slider, // Write values back to JS array ret = JS_NewArray(js); for (int i = 0; i < n && i < 4; i++) { - JS_SetPropertyUint32(js, ret, i, JS_NewFloat64(js, a[i])); + JS_SetPropertyNumber(js, ret, i, JS_NewFloat64(js, a[i])); } } else { float val = js2number(js, argv[1]); @@ -289,13 +289,13 @@ JSC_SCALL(imgui_intslider, int low = JS_IsNull(argv[2]) ? 0 : js2number(js, argv[2]); int high = JS_IsNull(argv[3]) ? 100 : js2number(js, argv[3]); - if (JS_IsArray(js, argv[1])) { + if (JS_IsArray(argv[1])) { int n = JS_ArrayLength(js, argv[1]); int a[4]; // Max 4 elements for SliderInt4 // Read values from JS array for (int i = 0; i < n && i < 4; i++) { - JSValue val = JS_GetPropertyUint32(js, argv[1], i); + JSValue val = JS_GetPropertyNumber(js, argv[1], i); double d; JS_ToFloat64(js, &d, val); a[i] = (int)d; @@ -317,7 +317,7 @@ JSC_SCALL(imgui_intslider, // Write values back to JS array ret = JS_NewArray(js); for (int i = 0; i < n && i < 4; i++) { - JS_SetPropertyUint32(js, ret, i, JS_NewInt32(js, a[i])); + JS_SetPropertyNumber(js, ret, i, JS_NewInt32(js, a[i])); } } else { int val = js2number(js, argv[1]); diff --git a/input.cm b/input.cm index 463a68a4..bf056ef0 100644 --- a/input.cm +++ b/input.cm @@ -147,69 +147,73 @@ function create_user(index, config) { // Pick user based on pairing policy function pick_user(canon) { + var picked = null + var old_down = null + var i = 0 if (length(_users) == 0) return null - + // For last_used: always user 0, just update active device if (_config.pairing == 'last_used') { - var user = _users[0] - + picked = _users[0] + // Only switch on button press, not axis/motion if (canon.kind == 'button' && canon.pressed) { - if (user.active_device != canon.device_id) { + if (picked.active_device != canon.device_id) { // Release all held actions when switching device - var old_down = user.router.down + old_down = picked.router.down arrfor(array(old_down), action => { if (old_down[action]) { - user.dispatch(action, { pressed: false, released: true, time: canon.time }) + picked.dispatch(action, { pressed: false, released: true, time: canon.time }) } }) - - user.active_device = canon.device_id - if (find(user.paired_devices, canon.device_id) == null) { - push(user.paired_devices, canon.device_id) + + picked.active_device = canon.device_id + if (find(picked.paired_devices, canon.device_id) == null) { + push(picked.paired_devices, canon.device_id) } } } - - return user + + return picked } - + // For explicit pairing: find user paired to this device - for (var i = 0; i < length(_users); i++) { + for (i = 0; i < length(_users); i++) { if (find(_users[i].paired_devices, canon.device_id) != null) { _users[i].active_device = canon.device_id return _users[i] } } - + // Unpaired device - could implement join logic here return null } // Configure the input system -function configure(opts) { - opts = opts || {} - +function configure(o) { + var opts = o || {} + var i = 0 + _config.max_users = opts.max_users || 1 _config.pairing = opts.pairing || 'last_used' _config.emacs = opts.emacs != false _config.gestures = opts.gestures != false - + if (opts.action_map) _config.action_map = opts.action_map if (opts.display_names) _config.display_names = opts.display_names if (opts.on_window) _window_callback = opts.on_window - + // Copy gesture config _config.swipe_min_dist = opts.swipe_min_dist _config.swipe_max_time = opts.swipe_max_time _config.pinch_threshold = opts.pinch_threshold - + // Create users _users = [] - for (var i = 0; i < _config.max_users; i++) { + for (i = 0; i < _config.max_users; i++) { push(_users, create_user(i, _config)) } - + _initialized = true } diff --git a/input/backends/sdl3.cm b/input/backends/sdl3.cm index 030b4887..bcb602e6 100644 --- a/input/backends/sdl3.cm +++ b/input/backends/sdl3.cm @@ -22,13 +22,15 @@ function parse_mods(mod) { // Translate SDL event to canonical format // Returns canonical event or null if not translatable function translate(evt) { + var control = null + // Keyboard if (evt.type == 'key_down' || evt.type == 'key_up') { if (evt.repeat) return null // Ignore key repeats - - var control = keycode_to_control(evt.key) + + control = keycode_to_control(evt.key) if (!control) return null - + return { kind: 'button', device_id: 'kbm', diff --git a/input/bindings.cm b/input/bindings.cm index 7aac9be3..833318a1 100644 --- a/input/bindings.cm +++ b/input/bindings.cm @@ -17,30 +17,30 @@ var controller_map = { joyconpair: 'joyconpair' } -function make(defaults, display_names) { - defaults = defaults || {} - display_names = display_names || {} - +function make(defs, names) { + var d = defs || {} + var n = names || {} + var bindings = { action_map: {}, _defaults: {}, - display_names: display_names, + display_names: n, is_rebinding: false, rebind_target: null } - + // Copy defaults - arrfor(array(defaults), function(key){ - var val = defaults[key] + arrfor(array(d), function(key){ + var val = d[key] bindings.action_map[key] = is_array(val) ? array(val) : [val] bindings._defaults[key] = array(bindings.action_map[key]) }) - + // Get actions that match a control bindings.get_actions = function(control) { return filter(array(this.action_map), action => find(this.action_map[action], control) != null) } - + // Get bindings for a specific device kind bindings.get_bindings_for_device = function(action, device_kind) { var all = this.action_map[action] || [] @@ -57,69 +57,76 @@ function make(defaults, display_names) { return true }) } - + // Get primary binding for display bindings.get_primary_binding = function(action, device_kind) { var device_bindings = this.get_bindings_for_device(action, device_kind) if (length(device_bindings)) return device_bindings[0] - + var all = this.action_map[action] || [] return length(all) > 0 ? all[0] : null } - + // Get icon for action bindings.get_icon_for_action = function(action, device_kind, gamepad_type) { var binding = this.get_primary_binding(action, device_kind) + var button = null + var key_map = null + var key = null + var gp_prefix = null + var is_ps = null + var gamepad_map = null + var icon = null if (!binding) return null - + if (device_kind == 'keyboard') { if (starts_with(binding, 'mouse_button_')) { - var button = replace(binding, 'mouse_button_', '') + button = replace(binding, 'mouse_button_', '') return 'ui/mouse/mouse_' + button + '.png' } else { - var key_map = { + key_map = { 'escape': 'escape', 'return': 'return', 'space': 'space', 'up': 'arrow_up', 'down': 'arrow_down', 'left': 'arrow_left', 'right': 'arrow_right' } - var key = key_map[binding] || binding + key = key_map[binding] || binding return 'ui/keyboard/keyboard_' + key + '.png' } } - + if (device_kind == 'gamepad' && gamepad_type) { - var prefix = controller_map[gamepad_type] || 'playstation' - var is_ps = find(['ps3', 'ps4', 'ps5'], gamepad_type) != null - - var gamepad_map = { + gp_prefix = controller_map[gamepad_type] || 'playstation' + is_ps = find(['ps3', 'ps4', 'ps5'], gamepad_type) != null + + gamepad_map = { 'gamepad_a': is_ps ? 'playstation_button_cross' : 'xbox_button_a', 'gamepad_b': is_ps ? 'playstation_button_circle' : 'xbox_button_b', 'gamepad_x': is_ps ? 'playstation_button_square' : 'xbox_button_x', 'gamepad_y': is_ps ? 'playstation_button_triangle' : 'xbox_button_y', - 'gamepad_dpup': prefix + '_dpad_up', - 'gamepad_dpdown': prefix + '_dpad_down', - 'gamepad_dpleft': prefix + '_dpad_left', - 'gamepad_dpright': prefix + '_dpad_right', - 'gamepad_l1': prefix + '_trigger_l1', - 'gamepad_r1': prefix + '_trigger_r1', - 'gamepad_l2': prefix + '_trigger_l2', - 'gamepad_r2': prefix + '_trigger_r2', + 'gamepad_dpup': gp_prefix + '_dpad_up', + 'gamepad_dpdown': gp_prefix + '_dpad_down', + 'gamepad_dpleft': gp_prefix + '_dpad_left', + 'gamepad_dpright': gp_prefix + '_dpad_right', + 'gamepad_l1': gp_prefix + '_trigger_l1', + 'gamepad_r1': gp_prefix + '_trigger_r1', + 'gamepad_l2': gp_prefix + '_trigger_l2', + 'gamepad_r2': gp_prefix + '_trigger_r2', 'gamepad_start': this._get_start_icon(gamepad_type) } - - var icon = gamepad_map[binding] - if (icon) return 'ui/' + prefix + '/' + icon + '.png' + + icon = gamepad_map[binding] + if (icon) return 'ui/' + gp_prefix + '/' + icon + '.png' } - + return null } - + bindings._get_start_icon = function(gamepad_type) { if (gamepad_type == 'ps3') return 'playstation3_button_start' if (gamepad_type == 'ps4') return 'playstation4_button_options' if (gamepad_type == 'ps5') return 'playstation5_button_options' return 'xbox_button_start' } - + // Start rebinding bindings.start_rebind = function(action) { if (!this.action_map[action]) return false @@ -127,73 +134,75 @@ function make(defaults, display_names) { this.rebind_target = action return true } - + // Complete rebinding with new control bindings.rebind = function(action, new_control, device_kind) { + var target = null + var i = 0 + var existing_kind = null if (!this.action_map[action]) return false - + // Remove from other actions arrfor(array(this.action_map), act => { var idx = search(this.action_map[act], new_control) if (idx >= 0) this.action_map[act] = array(array(this.action_map[act], 0, idx), array(this.action_map[act], idx+1)) }) - + // Clear existing bindings for this device kind - var target = this.action_map[action] - for (var i = length(target) - 1; i >= 0; i--) { - var existing_kind = starts_with(target[i], 'gamepad_') ? 'gamepad' : - starts_with(target[i], 'swipe_') ? 'touch' : 'keyboard' + target = this.action_map[action] + for (i = length(target) - 1; i >= 0; i--) { + existing_kind = starts_with(target[i], 'gamepad_') ? 'gamepad' : + starts_with(target[i], 'swipe_') ? 'touch' : 'keyboard' if (existing_kind == device_kind) this.action_map[action] = array(array(this.action_map[action], 0, i), array(this.action_map[action], i+1)) } - + // Add new binding this.action_map[action].unshift(new_control) this.is_rebinding = false this.rebind_target = null return true } - + // Cancel rebinding bindings.cancel_rebind = function() { this.is_rebinding = false this.rebind_target = null } - + // Save bindings - bindings.save = function(path) { - path = path || 'keybindings.json' - try { - io.slurpwrite(path, json.encode(this.action_map)) - return true - } catch(e) { - return false - } - } - - // Load bindings - bindings.load = function(path) { - path = path || 'keybindings.json' - try { - if (io.exists(path)) { - var data = json.decode(io.slurp(path)) - arrfor(array(data), key => { - if (this.action_map[key]) this.action_map[key] = data[key] - }) - return true - } - } catch(e) {} + bindings.save = function(p) { + var save_path = p || 'keybindings.json' + io.slurpwrite(save_path, json.encode(this.action_map)) + return true + } disruption { return false } - + + // Load bindings + bindings.load = function(p) { + var load_path = p || 'keybindings.json' + var data = null + if (io.exists(load_path)) { + data = json.decode(io.slurp(load_path)) + arrfor(array(data), key => { + if (this.action_map[key]) this.action_map[key] = data[key] + }) + return true + } + return false + } disruption { + return false + } + // Reset to defaults bindings.reset = function() { arrfor(array(this._defaults), key => { this.action_map[key] = array(this._defaults[key]) }) } - + return bindings } diff --git a/input/devices.cm b/input/devices.cm index 6cc2bed6..75d82792 100644 --- a/input/devices.cm +++ b/input/devices.cm @@ -6,12 +6,13 @@ var _devices = {} // Register a device from a canonical event function register(canon) { if (!canon || !canon.device_id) return - + var kind = null + if (!_devices[canon.device_id]) { - var kind = 'keyboard' + kind = 'keyboard' if (starts_with(canon.device_id, 'gp:')) kind = 'gamepad' else if (starts_with(canon.device_id, 'touch:')) kind = 'touch' - + _devices[canon.device_id] = { id: canon.device_id, kind: kind, diff --git a/input/router.cm b/input/router.cm index 5c592cb4..aa502145 100644 --- a/input/router.cm +++ b/input/router.cm @@ -15,62 +15,76 @@ var emacs_special = { } // Gesture stage - detects swipes and pinches from touchpad -function gesture_stage(config) { - config = config || {} - var min_swipe = config.swipe_min_dist || 30 - var max_time = config.swipe_max_time || 500 - var pinch_th = config.pinch_threshold || 10 - +function gesture_stage(cfg) { + var o = cfg || {} + var min_swipe = o.swipe_min_dist || 30 + var max_time = o.swipe_max_time || 500 + var pinch_th = o.pinch_threshold || 10 + var touches = {} var gesture_state = null var start_dist = 0 - + function dist(p1, p2) { var dx = p2[0] - p1[0] var dy = p2[1] - p1[1] return Math.sqrt(dx * dx + dy * dy) } - + return { process: function(events) { var output = [] - - for (var i = 0; i < length(events); i++) { - var ev = events[i] - + var i = 0 + var ev = null + var fingerId = null + var count = 0 + var fingers = null + var currentDist = 0 + var d = 0 + var touch = null + var dt = 0 + var dx = 0 + var dy = 0 + var absX = 0 + var absY = 0 + var dir = null + + for (i = 0; i < length(events); i++) { + ev = events[i] + // Only process gamepad touchpad events if (ev.control != 'gamepad_touchpad') { push(output, ev) continue } - - var fingerId = (ev.touchpad || 0) + '_' + (ev.finger || 0) - + + fingerId = (ev.touchpad || 0) + '_' + (ev.finger || 0) + if (ev.pressed) { touches[fingerId] = { pos: ev.pos, startPos: ev.pos, startTime: time.number() } - - var count = length(array(touches)) + + count = length(array(touches)) if (count == 1) gesture_state = 'single' else if (count == 2) { gesture_state = 'multi' - var fingers = Object.values(touches) + fingers = Object.values(touches) start_dist = dist(fingers[0].pos, fingers[1].pos) } } else if (ev.kind == 'axis') { if (touches[fingerId]) { touches[fingerId].pos = ev.pos - - var count = length(array(touches)) + + count = length(array(touches)) if (count == 2 && gesture_state == 'multi') { - var fingers = Object.values(touches) - var currentDist = dist(fingers[0].pos, fingers[1].pos) - var d = currentDist - start_dist - + fingers = Object.values(touches) + currentDist = dist(fingers[0].pos, fingers[1].pos) + d = currentDist - start_dist + if (Math.abs(d) >= pinch_th / 100) { push(output, { kind: 'gesture', @@ -88,21 +102,22 @@ function gesture_stage(config) { } else if (ev.released) { if (touches[fingerId]) { - var touch = touches[fingerId] - var count = length(array(touches)) - + touch = touches[fingerId] + count = length(array(touches)) + if (count == 1 && gesture_state == 'single') { - var dt = (time.number() - touch.startTime) * 1000 - var dx = ev.pos[0] - touch.startPos[0] - var dy = ev.pos[1] - touch.startPos[1] - + dt = (time.number() - touch.startTime) * 1000 + dx = ev.pos[0] - touch.startPos[0] + dy = ev.pos[1] - touch.startPos[1] + if (dt < max_time) { - var absX = Math.abs(dx), absY = Math.abs(dy) + absX = Math.abs(dx) + absY = Math.abs(dy) if (absX > min_swipe / 100 || absY > min_swipe / 100) { - var dir = absX > absY + dir = absX > absY ? (dx > 0 ? 'swipe_right' : 'swipe_left') : (dy > 0 ? 'swipe_down' : 'swipe_up') - + push(output, { kind: 'gesture', device_id: ev.device_id, @@ -114,13 +129,13 @@ function gesture_stage(config) { } } } - + delete touches[fingerId] if (length(array(touches)) == 0) gesture_state = null } } } - + return output } } @@ -129,50 +144,54 @@ function gesture_stage(config) { // Emacs stage - converts keyboard input to emacs chords function emacs_stage() { var prefix = null - + return { process: function(events) { var output = [] - - for (var i = 0; i < length(events); i++) { - var ev = events[i] - + var i = 0 + var ev = null + var notation = null + var chord = null + + for (i = 0; i < length(events); i++) { + ev = events[i] + // Only process keyboard button events if (ev.device_id != 'kbm' || ev.kind != 'button' || !ev.pressed) { push(output, ev) continue } - + /* if (find(valid_emacs_keys, ev.control) == null) { push(output, ev) continue } -*/ +*/ // Only process if we have modifiers OR waiting for chord if (!ev.mods?.ctrl && !ev.mods?.alt && !prefix) { push(output, ev) continue } - - var notation = "" + + notation = "" if (ev.mods?.ctrl) notation += "C-" if (ev.mods?.alt) notation += "M-" - + if (length(ev.control) == 1) { notation += lower(ev.control) } else { notation += emacs_special[ev.control] || ev.control } - + // Handle prefix keys if (notation == "C-x" || notation == "C-c") { prefix = notation continue // Consume, don't output } - + // Complete chord if we have prefix if (prefix) { - var chord = prefix + " " + notation + chord = prefix + " " + notation prefix = null push(output, { kind: 'chord', @@ -193,7 +212,7 @@ function emacs_stage() { }) } } - + return output } } @@ -202,34 +221,39 @@ function emacs_stage() { // Action mapping stage - converts controls to named actions function action_stage(bindings) { var down = {} - + return { down: down, process: function(events) { var output = [] - - for (var i = 0; i < length(events); i++) { - var ev = events[i] - + var i = 0 + var ev = null + var actions = null + var j = 0 + var action = null + + for (i = 0; i < length(events); i++) { + ev = events[i] + // Pass through non-button events if (ev.kind != 'button' && ev.kind != 'chord' && ev.kind != 'gesture') { push(output, ev) continue } - - var actions = bindings.get_actions(ev.control) - + + actions = bindings.get_actions(ev.control) + if (length(actions) == 0) { push(output, ev) continue } - - for (var j = 0; j < length(actions); j++) { - var action = actions[j] - + + for (j = 0; j < length(actions); j++) { + action = actions[j] + if (ev.pressed) down[action] = true else if (ev.released) down[action] = false - + push(output, { kind: 'action', device_id: ev.device_id, @@ -241,7 +265,7 @@ function action_stage(bindings) { }) } } - + return output } } @@ -251,55 +275,59 @@ function action_stage(bindings) { function delivery_stage(user) { return { process: function(events) { - for (var i = 0; i < length(events); i++) { - var ev = events[i] - + var i = 0 + var ev = null + + for (i = 0; i < length(events); i++) { + ev = events[i] + // Only deliver actions if (ev.kind != 'action') continue - + user.dispatch(ev.control, { pressed: ev.pressed, released: ev.released, time: ev.time }) } - + return events } } } // Create router with pipeline -function make(user, config) { - config = config || {} - +function make(user, cfg) { + var o = cfg || {} + var stages = [] var action = null - - if (config.gestures != false) { - push(stages, gesture_stage(config)) + + if (o.gestures != false) { + push(stages, gesture_stage(o)) } - - if (config.emacs != false) { + + if (o.emacs != false) { push(stages, emacs_stage()) } - + action = action_stage(user.bindings) push(stages, action) push(stages, delivery_stage(user)) - + return { stages: stages, - + down() { return action.down }, - + handle: function(canon) { var events = [canon] - - for (var i = 0; i < length(this.stages); i++) { + var i = 0 + + for (i = 0; i < length(this.stages); i++) { events = this.stages[i].process(events) } - + return events } } diff --git a/layout.c b/layout.c index ec114d72..d814f6f2 100644 --- a/layout.c +++ b/layout.c @@ -39,9 +39,9 @@ static JSValue js_layout_set_size(JSContext *js, JSValueConst self, int argc, JS double width = 0, height = 0; // Check if it's an array (for backwards compatibility) - if (JS_IsArray(js, argv[1])) { - JSValue width_val = JS_GetPropertyUint32(js, argv[1], 0); - JSValue height_val = JS_GetPropertyUint32(js, argv[1], 1); + if (JS_IsArray(argv[1])) { + JSValue width_val = JS_GetPropertyNumber(js, argv[1], 0); + JSValue height_val = JS_GetPropertyNumber(js, argv[1], 1); JS_ToFloat64(js, &width, width_val); JS_ToFloat64(js, &height, height_val); JS_FreeValue(js, width_val); @@ -162,7 +162,7 @@ static JSValue js_layout_reset(JSContext *js, JSValueConst self, int argc, JSVal static void js_layout_finalizer(JSRuntime *rt, JSValue val) { lay_context *ctx = JS_GetOpaque(val, js_layout_class_id); lay_destroy_context(ctx); - js_free_rt(rt, ctx); + js_free_rt(ctx); } static JSClassDef js_layout_class = { @@ -190,7 +190,7 @@ static const JSCFunctionListEntry js_layout_funcs[] = { CELL_USE_INIT( JS_NewClassID(&js_layout_class_id); - JS_NewClass(JS_GetRuntime(js), js_layout_class_id, &js_layout_class); + JS_NewClass(js, js_layout_class_id, &js_layout_class); JSValue proto = JS_NewObject(js); JS_SetPropertyFunctionList(js, proto, js_layout_proto_funcs, sizeof(js_layout_proto_funcs) / sizeof(JSCFunctionListEntry)); diff --git a/line2d.cm b/line2d.cm index c3f533db..cbfbcf01 100644 --- a/line2d.cm +++ b/line2d.cm @@ -78,117 +78,147 @@ function build_polyline_mesh(line) { // Transform points if in local space var pts = [] - for (var i = 0; i < length(points); i++) { - var p = points[i] + var i = 0 + var p = null + var dx = 0 + var dy = 0 + + for (i = 0; i < length(points); i++) { + p = points[i] if (points_space == 'local') { push(pts, {x: p.x + pos.x, y: p.y + pos.y}) } else { push(pts, {x: p.x, y: p.y}) } } - + // Calculate cumulative distances var cumulative = [0] - for (var i = 1; i < length(pts); i++) { - var dx = pts[i].x - pts[i-1].x - var dy = pts[i].y - pts[i-1].y + for (i = 1; i < length(pts); i++) { + dx = pts[i].x - pts[i-1].x + dy = pts[i].y - pts[i-1].y push(cumulative, cumulative[i-1] + math.sqrt(dx*dx + dy*dy)) } var total_length = cumulative[length(cumulative) - 1] - + // Build triangle strip mesh var verts = [] var indices = [] - + // Get width at point i - function get_width(i) { - if (widths && length(widths) > i) return widths[i] + function get_width(idx) { + if (widths && length(widths) > idx) return widths[idx] return width } - + // Get U coordinate at point i - function get_u(i) { + var seg_idx = 0 + var seg_len = 0 + function get_u(idx) { if (uv_mode == 'stretch') { - return total_length > 0 ? cumulative[i] / total_length : 0 + return total_length > 0 ? cumulative[idx] / total_length : 0 } else if (uv_mode == 'per_segment') { - if (i == 0) return 0 - var seg_idx = i - 1 - var seg_len = cumulative[i] - cumulative[i-1] + if (idx == 0) return 0 + seg_idx = idx - 1 + seg_len = cumulative[idx] - cumulative[idx-1] return 1 // Each segment ends at u=1 } else { // repeat (default) - return cumulative[i] * u_per_unit + u_offset + return cumulative[idx] * u_per_unit + u_offset } } - + // Calculate normals at each point var normals = [] - for (var i = 0; i < length(pts); i++) { - var prev = i > 0 ? pts[i-1] : (closed ? pts[length(pts)-1] : null) - var curr = pts[i] - var next = i < length(pts)-1 ? pts[i+1] : (closed ? pts[0] : null) - - var n = {x: 0, y: 0} - + var prev = null + var curr = null + var next = null + var n = null + var d1x = 0 + var d1y = 0 + var d2x = 0 + var d2y = 0 + var len1 = 0 + var len2 = 0 + var n1x = 0 + var n1y = 0 + var n2x = 0 + var n2y = 0 + var nlen = 0 + var dot = 0 + var miter_scale = 0 + var len = 0 + + for (i = 0; i < length(pts); i++) { + prev = i > 0 ? pts[i-1] : (closed ? pts[length(pts)-1] : null) + curr = pts[i] + next = i < length(pts)-1 ? pts[i+1] : (closed ? pts[0] : null) + + n = {x: 0, y: 0} + if (prev && next) { // Middle point - average normals - var d1x = curr.x - prev.x - var d1y = curr.y - prev.y - var d2x = next.x - curr.x - var d2y = next.y - curr.y - - var len1 = math.sqrt(d1x*d1x + d1y*d1y) - var len2 = math.sqrt(d2x*d2x + d2y*d2y) - + d1x = curr.x - prev.x + d1y = curr.y - prev.y + d2x = next.x - curr.x + d2y = next.y - curr.y + + len1 = math.sqrt(d1x*d1x + d1y*d1y) + len2 = math.sqrt(d2x*d2x + d2y*d2y) + if (len1 > 0.0001) { d1x /= len1; d1y /= len1 } if (len2 > 0.0001) { d2x /= len2; d2y /= len2 } - + // Normals (perpendicular) - var n1x = -d1y, n1y = d1x - var n2x = -d2y, n2y = d2x - + n1x = -d1y + n1y = d1x + n2x = -d2y + n2y = d2x + // Average n.x = n1x + n2x n.y = n1y + n2y - var nlen = math.sqrt(n.x*n.x + n.y*n.y) + nlen = math.sqrt(n.x*n.x + n.y*n.y) if (nlen > 0.0001) { n.x /= nlen; n.y /= nlen } - + // Miter correction - var dot = n1x * n.x + n1y * n.y + dot = n1x * n.x + n1y * n.y if (dot > 0.0001) { - var miter_scale = 1 / dot + miter_scale = 1 / dot if (miter_scale > miter_limit) miter_scale = miter_limit n.x *= miter_scale n.y *= miter_scale } } else if (next) { // Start point - var dx = next.x - curr.x - var dy = next.y - curr.y - var len = math.sqrt(dx*dx + dy*dy) + dx = next.x - curr.x + dy = next.y - curr.y + len = math.sqrt(dx*dx + dy*dy) if (len > 0.0001) { dx /= len; dy /= len } n.x = -dy n.y = dx } else if (prev) { // End point - var dx = curr.x - prev.x - var dy = curr.y - prev.y - var len = math.sqrt(dx*dx + dy*dy) + dx = curr.x - prev.x + dy = curr.y - prev.y + len = math.sqrt(dx*dx + dy*dy) if (len > 0.0001) { dx /= len; dy /= len } n.x = -dy n.y = dx } - + push(normals, n) } - + // Generate vertices (2 per point - left and right of line) - for (var i = 0; i < length(pts); i++) { - var p = pts[i] - var n = normals[i] - var w = get_width(i) * 0.5 - var u = get_u(i) - + var w = 0 + var u = 0 + for (i = 0; i < length(pts); i++) { + p = pts[i] + n = normals[i] + w = get_width(i) * 0.5 + u = get_u(i) + // Left vertex (v=0) push(verts, { x: p.x + n.x * w, @@ -197,7 +227,7 @@ function build_polyline_mesh(line) { v: v_offset, r: 1, g: 1, b: 1, a: 1 }) - + // Right vertex (v=1) push(verts, { x: p.x - n.x * w, @@ -207,10 +237,11 @@ function build_polyline_mesh(line) { r: 1, g: 1, b: 1, a: 1 }) } - + // Generate indices (triangle strip as triangles) - for (var i = 0; i < length(pts) - 1; i++) { - var base = i * 2 + var base = 0 + for (i = 0; i < length(pts) - 1; i++) { + base = i * 2 // First triangle push(indices, base + 0) push(indices, base + 1) @@ -220,10 +251,11 @@ function build_polyline_mesh(line) { push(indices, base + 3) push(indices, base + 2) } - + // Handle closed path + var last = 0 if (closed && length(pts) > 2) { - var last = (length(pts) - 1) * 2 + last = (length(pts) - 1) * 2 push(indices, last + 0) push(indices, last + 1) push(indices, 0) @@ -234,11 +266,11 @@ function build_polyline_mesh(line) { // Add round caps if requested if (!closed && cap == 'round') { - add_round_cap(verts, indices, pts[0], normals[0], get_width(0), get_u(0), v_offset, v_scale, true) - add_round_cap(verts, indices, pts[length(pts)-1], normals[length(pts)-1], get_width(length(pts)-1), get_u(length(pts)-1), v_offset, v_scale, false) + add_round_cap({verts: verts, indices: indices, p: pts[0], n: normals[0], width: get_width(0), u: get_u(0), v_offset: v_offset, v_scale: v_scale, is_start: true}) + add_round_cap({verts: verts, indices: indices, p: pts[length(pts)-1], n: normals[length(pts)-1], width: get_width(length(pts)-1), u: get_u(length(pts)-1), v_offset: v_offset, v_scale: v_scale, is_start: false}) } else if (!closed && cap == 'square') { - add_square_cap(verts, indices, pts[0], normals[0], get_width(0), get_u(0), v_offset, v_scale, true, pts[1]) - add_square_cap(verts, indices, pts[length(pts)-1], normals[length(pts)-1], get_width(length(pts)-1), get_u(length(pts)-1), v_offset, v_scale, false, pts[length(pts)-2]) + add_square_cap({verts: verts, indices: indices, p: pts[0], n: normals[0], width: get_width(0), u: get_u(0), v_offset: v_offset, v_scale: v_scale, is_start: true, adjacent: pts[1]}) + add_square_cap({verts: verts, indices: indices, p: pts[length(pts)-1], n: normals[length(pts)-1], width: get_width(length(pts)-1), u: get_u(length(pts)-1), v_offset: v_offset, v_scale: v_scale, is_start: false, adjacent: pts[length(pts)-2]}) } return { @@ -249,15 +281,25 @@ function build_polyline_mesh(line) { } } -function add_round_cap(verts, indices, p, n, width, u, v_offset, v_scale, is_start) { - var w = width * 0.5 +function add_round_cap(opts) { + var verts = opts.verts + var indices = opts.indices + var p = opts.p + var n = opts.n + var rc_width = opts.width + var u = opts.u + var v_offset = opts.v_offset + var v_scale = opts.v_scale + var is_start = opts.is_start + + var w = rc_width * 0.5 var segments = 8 var base_idx = length(verts) - + // Direction along the line var dx = is_start ? -n.y : n.y var dy = is_start ? n.x : -n.x - + // Center vertex push(verts, { x: p.x, @@ -266,14 +308,18 @@ function add_round_cap(verts, indices, p, n, width, u, v_offset, v_scale, is_sta v: 0.5 * v_scale + v_offset, r: 1, g: 1, b: 1, a: 1 }) - + // Arc vertices var start_angle = is_start ? math.arc_tangent(n.y, n.x) : math.arc_tangent(-n.y, -n.x) - for (var i = 0; i <= segments; i++) { - var angle = start_angle + (i / segments) * 3.14159 - var cx = math.cosine(angle) - var cy = math.sine(angle) - + var i = 0 + var angle = 0 + var cx = 0 + var cy = 0 + for (i = 0; i <= segments; i++) { + angle = start_angle + (i / segments) * 3.14159 + cx = math.cosine(angle) + cy = math.sine(angle) + push(verts, { x: p.x + cx * w, y: p.y + cy * w, @@ -282,30 +328,41 @@ function add_round_cap(verts, indices, p, n, width, u, v_offset, v_scale, is_sta r: 1, g: 1, b: 1, a: 1 }) } - + // Fan triangles - for (var i = 0; i < segments; i++) { + for (i = 0; i < segments; i++) { push(indices, base_idx) push(indices, base_idx + 1 + i) push(indices, base_idx + 2 + i) } } -function add_square_cap(verts, indices, p, n, width, u, v_offset, v_scale, is_start, adjacent) { - var w = width * 0.5 +function add_square_cap(opts) { + var verts = opts.verts + var indices = opts.indices + var p = opts.p + var n = opts.n + var sc_width = opts.width + var u = opts.u + var v_offset = opts.v_offset + var v_scale = opts.v_scale + var is_start = opts.is_start + var adjacent = opts.adjacent + + var w = sc_width * 0.5 var base_idx = length(verts) - + // Direction along the line (away from adjacent point) var dx = p.x - adjacent.x var dy = p.y - adjacent.y var len = math.sqrt(dx*dx + dy*dy) if (len > 0.0001) { dx /= len; dy /= len } - + // Extend by half width var ext = w var ex = p.x + dx * ext var ey = p.y + dy * ext - + // Four corners of the cap push(verts, {x: p.x + n.x * w, y: p.y + n.y * w, u: u, v: v_offset, r: 1, g: 1, b: 1, a: 1}) push(verts, {x: p.x - n.x * w, y: p.y - n.y * w, u: u, v: v_scale + v_offset, r: 1, g: 1, b: 1, a: 1}) @@ -388,9 +445,9 @@ var line2d = { return make_line(props) }, - line: function(x1, y1, x2, y2, props) { + line: function(from, to, props) { var p = props || {} - p.points = [{x: x1, y: y1}, {x: x2, y: y2}] + p.points = [{x: from.x, y: from.y}, {x: to.x, y: to.y}] return make_line(p) } } diff --git a/math.c b/math.c index 3104572e..00b11972 100644 --- a/math.c +++ b/math.c @@ -10,7 +10,7 @@ // Utility function to get number from array index static double js_getnum_uint32(JSContext *js, JSValue v, unsigned int i) { - JSValue val = JS_GetPropertyUint32(js,v,i); + JSValue val = JS_GetPropertyNumber(js,v,i); double ret = js2number(js, val); JS_FreeValue(js,val); return ret; @@ -30,7 +30,7 @@ static float *js2floats(JSContext *js, JSValue v, size_t *len) static JSValue floats2array(JSContext *js, float *vals, size_t len) { JSValue arr = JS_NewArray(js); for (size_t i = 0; i < len; i++) { - JS_SetPropertyUint32(js, arr, i, number2js(js, vals[i])); + JS_SetPropertyNumber(js, arr, i, number2js(js, vals[i])); } return arr; } @@ -94,7 +94,7 @@ JSC_CCALL(math_norm, JSValue newarr = JS_NewArray(js); for (int i = 0; i < len; i++) - JS_SetPropertyUint32(js, newarr, i, number2js(js,js_getnum_uint32(js, argv[0],i)/length)); + JS_SetPropertyNumber(js, newarr, i, number2js(js,js_getnum_uint32(js, argv[0],i)/length)); ret = newarr; ) @@ -238,7 +238,7 @@ JSC_CCALL(math_from_to, int i = 0; for (double val = start; val <= end; val += step) { if (val == end && !inclusive) break; - JS_SetPropertyUint32(js, jsarr, i++, number2js(js, val)); + JS_SetPropertyNumber(js, jsarr, i++, number2js(js, val)); } return jsarr; diff --git a/mersenne.c b/mersenne.c index 430b44f0..51af5c83 100644 --- a/mersenne.c +++ b/mersenne.c @@ -69,7 +69,7 @@ static JSClassID js_mersenne_class_id; static void js_mersenne_finalizer(JSRuntime *rt, JSValue val) { MTRand *mrand = JS_GetOpaque(val, js_mersenne_class_id); - js_free_rt(rt, mrand); + js_free_rt(mrand); } static JSClassDef js_mersenne_class = { @@ -131,7 +131,7 @@ static JSValue js_mersenne_use_call(JSContext *js, JSValueConst new_target, int CELL_USE_INIT( JS_NewClassID(&js_mersenne_class_id); - JS_NewClass(JS_GetRuntime(js), js_mersenne_class_id, &js_mersenne_class); + JS_NewClass(js, js_mersenne_class_id, &js_mersenne_class); JSValue proto = JS_NewObject(js); JS_SetPropertyFunctionList(js, proto, js_mersenne_funcs, sizeof(js_mersenne_funcs)/sizeof(JSCFunctionListEntry)); diff --git a/particles2d.cm b/particles2d.cm index a392c323..0eeb27bf 100644 --- a/particles2d.cm +++ b/particles2d.cm @@ -43,25 +43,31 @@ var emitters = { // Update an emitter and its particles update: function(emitter, dt) { // Spawn new particles + var pp = 0 if (emitter.rate > 0) { emitter.spawn_timer = (emitter.spawn_timer || 0) + dt - var pp = 1 / emitter.rate + pp = 1 / emitter.rate while (emitter.spawn_timer > pp) { emitter.spawn_timer -= pp emitters.spawn(emitter) } } - + // Update existing particles - for (var i = length(emitter.particles) - 1; i >= 0; i--) { - var p = emitter.particles[i] + var i = 0 + var p = null + var grow_for = 0 + var shrink_for = 0 + var alpha = 0 + for (i = length(emitter.particles) - 1; i >= 0; i--) { + p = emitter.particles[i] p.time += dt p.pos.x += p.velocity.x * dt p.pos.y += p.velocity.y * dt - + // Scale animation - var grow_for = emitter.grow_for || 0.3 - var shrink_for = emitter.shrink_for || 0.5 + grow_for = emitter.grow_for || 0.3 + shrink_for = emitter.shrink_for || 0.5 if (p.time < grow_for) { p.scale = lerp(0, p.max_scale, p.time / grow_for) } else if (p.time > p.life - shrink_for) { @@ -69,9 +75,9 @@ var emitters = { } else { p.scale = p.max_scale } - + // Alpha fade - var alpha = 1 + alpha = 1 if (p.time > p.life * 0.7) { alpha = 1 - (p.time - p.life * 0.7) / (p.life * 0.3) } diff --git a/playdate.cm b/playdate.cm index cbc60920..ebbe1f2b 100644 --- a/playdate.cm +++ b/playdate.cm @@ -82,46 +82,36 @@ PlaydateBackend.prototype.execute = function(commands) { } PlaydateBackend.prototype.execute_command = function(cmd) { - switch (cmd.cmd) { - case 'begin_render': - this.cmd_begin_render(cmd) - break - case 'end_render': - this.cmd_end_render() - break - case 'set_camera': - this.cmd_set_camera(cmd) - break - case 'draw_batch': - this.cmd_draw_batch(cmd) - break - case 'shader_pass': - this.cmd_shader_pass(cmd) // DEGRADES - break - case 'apply_mask': - this.cmd_apply_mask(cmd) // NATIVE! - break - case 'composite': - this.cmd_composite(cmd) - break - case 'blit': - this.cmd_blit(cmd) - break - case 'clear': - this.cmd_clear(cmd) - break - case 'present': - // Nothing to do, display updates automatically - break - default: - console.error(`Unknown command: ${cmd.cmd}`) + if (cmd.cmd == 'begin_render') { + this.cmd_begin_render(cmd) + } else if (cmd.cmd == 'end_render') { + this.cmd_end_render() + } else if (cmd.cmd == 'set_camera') { + this.cmd_set_camera(cmd) + } else if (cmd.cmd == 'draw_batch') { + this.cmd_draw_batch(cmd) + } else if (cmd.cmd == 'shader_pass') { + this.cmd_shader_pass(cmd) // DEGRADES + } else if (cmd.cmd == 'apply_mask') { + this.cmd_apply_mask(cmd) // NATIVE! + } else if (cmd.cmd == 'composite') { + this.cmd_composite(cmd) + } else if (cmd.cmd == 'blit') { + this.cmd_blit(cmd) + } else if (cmd.cmd == 'clear') { + this.cmd_clear(cmd) + } else if (cmd.cmd == 'present') { + // Nothing to do, display updates automatically + } else { + log.console(`Unknown command: ${cmd.cmd}`) } } PlaydateBackend.prototype.cmd_begin_render = function(cmd) { var target = cmd.target var clear = cmd.clear - + var pattern = null + if (target == 'screen') { // Render to screen framebuffer this.current_target = 'screen' @@ -130,10 +120,10 @@ PlaydateBackend.prototype.cmd_begin_render = function(cmd) { // Render to bitmap this.current_target = target this.pd.graphics.pushContext(target.bitmap) - + if (clear) { // Clear with color (Playdate is 1-bit, so color becomes pattern/dither) - var pattern = this.color_to_dither_pattern(clear) + pattern = this.color_to_dither_pattern(clear) this.pd.graphics.setDitherPattern(pattern) this.pd.graphics.fillRect(0, 0, target.width, target.height) } @@ -156,44 +146,54 @@ PlaydateBackend.prototype.cmd_set_camera = function(cmd) { PlaydateBackend.prototype.cmd_draw_batch = function(cmd) { var geometry = cmd.geometry var material = cmd.material || {} - + // Get image (Playdate uses LCDBitmap for textures) var image = this.get_image(material.texture || 'white') if (!image) return - + // Set draw mode based on material var draw_mode = this.material_to_draw_mode(material) this.pd.graphics.setImageDrawMode(draw_mode) - + // Draw each sprite in the batch - for (var i = 0; i < length(geometry.indices); i += 6) { + var i = 0 + var vert_idx = 0 + var v = null + var screen_pos = null + var v2 = null + var width = 0 + var height = 0 + var scale = 0 + var alpha_pattern = null + var scaled = null + for (i = 0; i < length(geometry.indices); i += 6) { // Each sprite is 2 triangles = 6 indices = 4 vertices - var vert_idx = geometry.indices[i] - var v = geometry.vertices[vert_idx] - + vert_idx = geometry.indices[i] + v = geometry.vertices[vert_idx] + // Transform by camera - var screen_pos = this.world_to_screen(v.pos, this.current_camera) - + screen_pos = this.world_to_screen(v.pos, this.current_camera) + // Get sprite size from vertices - var v2 = geometry.vertices[vert_idx + 2] - var width = v2.pos[0] - v.pos[0] - var height = v2.pos[1] - v.pos[1] - + v2 = geometry.vertices[vert_idx + 2] + width = v2.pos[0] - v.pos[0] + height = v2.pos[1] - v.pos[1] + // Transform size by camera zoom - var scale = this.get_camera_scale(this.current_camera) + scale = this.get_camera_scale(this.current_camera) width *= scale height *= scale - + // Apply vertex color as dither pattern (best we can do on 1-bit) if (v.color.a < 1.0) { - var alpha_pattern = this.alpha_to_dither_pattern(v.color.a) + alpha_pattern = this.alpha_to_dither_pattern(v.color.a) this.pd.graphics.setDitherPattern(alpha_pattern) } - + // Draw image if (width != image.width || height != image.height) { // Need scaling - var scaled = image.scaledImage(width / image.width, height / image.height) + scaled = image.scaledImage(width / image.width, height / image.height) this.pd.graphics.drawBitmap(scaled, screen_pos[0], screen_pos[1]) } else { this.pd.graphics.drawBitmap(image, screen_pos[0], screen_pos[1]) @@ -204,35 +204,35 @@ PlaydateBackend.prototype.cmd_draw_batch = function(cmd) { PlaydateBackend.prototype.cmd_shader_pass = function(cmd) { // NO SHADERS ON PLAYDATE // Degrade gracefully based on shader type - + var shader = cmd.shader var input = cmd.input var params = cmd.params - + if (shader == 'threshold') { // Threshold: Just copy input (or could dither based on threshold) - console.warn('Threshold shader not supported on Playdate, copying') + log.console('Threshold shader not supported on Playdate, copying') this.copy_bitmap(input.bitmap, this.current_target.bitmap) } else if (shader == 'gaussian_blur') { // Blur: Box blur in CPU (slow but possible) - console.warn('Blur shader using CPU fallback') + log.console('Blur shader using CPU fallback') this.cpu_box_blur(input.bitmap, this.current_target.bitmap, params.radius || 5) } else if (shader == 'add_textures') { // Additive blend: Use XOR draw mode (not perfect but interesting) - console.warn('Additive blend approximated with XOR') + log.console('Additive blend approximated with XOR') this.pd.graphics.setImageDrawMode(this.pd.graphics.kDrawModeXOR) this.pd.graphics.drawBitmap(input.bitmap, 0, 0) } else if (shader == 'crt_filter') { // CRT: Not possible, just copy - console.warn('CRT filter not supported on Playdate, copying') + log.console('CRT filter not supported on Playdate, copying') this.copy_bitmap(input.bitmap, this.current_target.bitmap) } else { // Unknown shader: copy - console.warn(`Shader ${shader} not supported on Playdate, copying`) + log.console(`Shader ${shader} not supported on Playdate, copying`) this.copy_bitmap(input.bitmap, this.current_target.bitmap) } } @@ -242,23 +242,24 @@ PlaydateBackend.prototype.cmd_apply_mask = function(cmd) { var content = cmd.content_texture.bitmap var mask = cmd.mask_texture.bitmap var invert = cmd.invert - + var inverted = null + if (invert) { // Invert mask first - var inverted = this.pd.graphics.newBitmap(mask.width, mask.height) + inverted = this.pd.graphics.newBitmap(mask.width, mask.height) this.pd.graphics.pushContext(inverted) this.pd.graphics.setImageDrawMode(this.pd.graphics.kDrawModeInverted) this.pd.graphics.drawBitmap(mask, 0, 0) this.pd.graphics.popContext() mask = inverted } - + // Set mask on content bitmap content.setMask(mask) - + // Draw masked content to current target this.pd.graphics.drawBitmap(content, 0, 0) - + // Clear mask (don't leave it set) content.setMask(null) } @@ -284,14 +285,15 @@ PlaydateBackend.prototype.cmd_blit = function(cmd) { var bitmap = cmd.texture.bitmap var dst_rect = cmd.dst_rect var filter = cmd.filter - + // Scale bitmap to fit dst_rect var scale_x = dst_rect.width / bitmap.width var scale_y = dst_rect.height / bitmap.height - + var scaled = null + if (scale_x != 1.0 || scale_y != 1.0) { // Playdate only supports nearest-neighbor scaling - var scaled = bitmap.scaledImage(scale_x, scale_y) + scaled = bitmap.scaledImage(scale_x, scale_y) this.pd.graphics.drawBitmap(scaled, dst_rect.x, dst_rect.y) } else { this.pd.graphics.drawBitmap(bitmap, dst_rect.x, dst_rect.y) @@ -400,36 +402,44 @@ PlaydateBackend.prototype.get_image = function(name) { PlaydateBackend.prototype.cpu_box_blur = function(src, dst, radius) { // Simple box blur implementation in CPU // This is SLOW but correct - + var width = src.width var height = src.height - + // Get pixel data (Playdate API for accessing bitmap pixels) var src_data = src.getData() var dst_data = dst.getData() - - for (var y = 0; y < height; y++) { - for (var x = 0; x < width; x++) { - var sum = 0 - var count = 0 - + var y = 0 + var x = 0 + var sum = 0 + var count = 0 + var dy = 0 + var dx = 0 + var sx = 0 + var sy = 0 + + for (y = 0; y < height; y++) { + for (x = 0; x < width; x++) { + sum = 0 + count = 0 + // Sample neighborhood - for (var dy = -radius; dy <= radius; dy++) { - for (var dx = -radius; dx <= radius; dx++) { - var sx = x + dx - var sy = y + dy - + for (dy = -radius; dy <= radius; dy++) { + for (dx = -radius; dx <= radius; dx++) { + sx = x + dx + sy = y + dy + if (sx >= 0 && sx < width && sy >= 0 && sy < height) { sum += src_data[sy * width + sx] count++ } } } - + dst_data[y * width + x] = sum / count } } - + dst.setData(dst_data) } diff --git a/prosperon.c b/prosperon.c index f74d9cbf..df674daa 100644 --- a/prosperon.c +++ b/prosperon.c @@ -7,10 +7,10 @@ colorf js2color(JSContext *js,JSValue v) { colorf color = {1,1,1,1}; // Default to white - if (JS_IsArray(js, v)) { + if (JS_IsArray(v)) { // Handle array format: [r, g, b, a] JSValue c[4]; - for (int i = 0; i < 4; i++) c[i] = JS_GetPropertyUint32(js,v,i); + for (int i = 0; i < 4; i++) c[i] = JS_GetPropertyNumber(js,v,i); color.r = js2number(js,c[0]); color.g = js2number(js,c[1]); @@ -31,10 +31,10 @@ colorf js2color(JSContext *js,JSValue v) { JSValue color2js(JSContext *js, colorf color) { JSValue arr = JS_NewArray(js); - JS_SetPropertyUint32(js, arr,0,number2js(js,(double)color.r)); - JS_SetPropertyUint32(js, arr,1,number2js(js,(double)color.g)); - JS_SetPropertyUint32(js, arr,2,number2js(js,(double)color.b)); - JS_SetPropertyUint32(js, arr,3,number2js(js,(double)color.a)); + JS_SetPropertyNumber(js, arr,0,number2js(js,(double)color.r)); + JS_SetPropertyNumber(js, arr,1,number2js(js,(double)color.g)); + JS_SetPropertyNumber(js, arr,2,number2js(js,(double)color.b)); + JS_SetPropertyNumber(js, arr,3,number2js(js,(double)color.a)); return arr; } @@ -43,9 +43,9 @@ HMM_Vec2 js2vec2(JSContext *js,JSValue v) HMM_Vec2 v2; // Check if it's an array - if (JS_IsArray(js, v)) { - { JSValue val = JS_GetPropertyUint32(js,v,0); v2.X = js2number(js, val); JS_FreeValue(js,val); } - { JSValue val = JS_GetPropertyUint32(js,v,1); v2.Y = js2number(js, val); JS_FreeValue(js,val); } + if (JS_IsArray(v)) { + { JSValue val = JS_GetPropertyNumber(js,v,0); v2.X = js2number(js, val); JS_FreeValue(js,val); } + { JSValue val = JS_GetPropertyNumber(js,v,1); v2.Y = js2number(js, val); JS_FreeValue(js,val); } } else { // Try to get x,y properties from object JSValue x_val = JS_GetPropertyStr(js, v, "x"); @@ -64,9 +64,9 @@ HMM_Vec2 js2vec2(JSContext *js,JSValue v) HMM_Vec3 js2vec3(JSContext *js,JSValue v) { HMM_Vec3 v3; - { JSValue val = JS_GetPropertyUint32(js, v,0); v3.x = js2number(js, val); JS_FreeValue(js,val); } - { JSValue val = JS_GetPropertyUint32(js, v,1); v3.y = js2number(js, val); JS_FreeValue(js,val); } - { JSValue val = JS_GetPropertyUint32(js, v,2); v3.z = js2number(js, val); JS_FreeValue(js,val); } + { JSValue val = JS_GetPropertyNumber(js, v,0); v3.x = js2number(js, val); JS_FreeValue(js,val); } + { JSValue val = JS_GetPropertyNumber(js, v,1); v3.y = js2number(js, val); JS_FreeValue(js,val); } + { JSValue val = JS_GetPropertyNumber(js, v,2); v3.z = js2number(js, val); JS_FreeValue(js,val); } return v3; } @@ -75,7 +75,7 @@ float *js2floats(JSContext *js, JSValue v, size_t *len) *len = JS_ArrayLength(js,v); float *arr = malloc(sizeof(float)* *len); for (int i = 0; i < *len; i++) - { JSValue val = JS_GetPropertyUint32(js,v,i); arr[i] = js2number(js, val); JS_FreeValue(js,val); } + { JSValue val = JS_GetPropertyNumber(js,v,i); arr[i] = js2number(js, val); JS_FreeValue(js,val); } return arr; } @@ -84,14 +84,14 @@ double *js2doubles(JSContext *js, JSValue v, size_t *len) *len = JS_ArrayLength(js,v); double *arr = malloc(sizeof(double)* *len); for (int i = 0; i < *len; i++) - { JSValue val = JS_GetPropertyUint32(js,v,i); arr[i] = js2number(js, val); JS_FreeValue(js,val); } + { JSValue val = JS_GetPropertyNumber(js,v,i); arr[i] = js2number(js, val); JS_FreeValue(js,val); } return arr; } HMM_Vec3 js2vec3f(JSContext *js, JSValue v) { HMM_Vec3 vec; - if (JS_IsArray(js, v)) + if (JS_IsArray(v)) return js2vec3(js,v); else vec.x = vec.y = vec.z = js2number(js,v); @@ -101,9 +101,9 @@ HMM_Vec3 js2vec3f(JSContext *js, JSValue v) JSValue vec32js(JSContext *js, HMM_Vec3 v) { JSValue array = JS_NewArray(js); - JS_SetPropertyUint32(js, array,0,number2js(js,v.x)); - JS_SetPropertyUint32(js, array,1,number2js(js,v.y)); - JS_SetPropertyUint32(js, array,2,number2js(js,v.z)); + JS_SetPropertyNumber(js, array,0,number2js(js,v.x)); + JS_SetPropertyNumber(js, array,1,number2js(js,v.y)); + JS_SetPropertyNumber(js, array,2,number2js(js,v.z)); return array; } @@ -115,10 +115,10 @@ JSValue vec3f2js(JSContext *js, HMM_Vec3 v) JSValue quat2js(JSContext *js, HMM_Quat q) { JSValue arr = JS_NewArray(js); - JS_SetPropertyUint32(js, arr, 0, number2js(js,q.x)); - JS_SetPropertyUint32(js, arr,1,number2js(js,q.y)); - JS_SetPropertyUint32(js, arr,2,number2js(js,q.z)); - JS_SetPropertyUint32(js, arr,3,number2js(js,q.w)); + JS_SetPropertyNumber(js, arr, 0, number2js(js,q.x)); + JS_SetPropertyNumber(js, arr,1,number2js(js,q.y)); + JS_SetPropertyNumber(js, arr,2,number2js(js,q.z)); + JS_SetPropertyNumber(js, arr,3,number2js(js,q.w)); return arr; } @@ -126,7 +126,7 @@ HMM_Vec4 js2vec4(JSContext *js, JSValue v) { HMM_Vec4 v4; for (int i = 0; i < 4; i++) - { JSValue val = JS_GetPropertyUint32(js, v,i); v4.e[i] = js2number(js, val); JS_FreeValue(js,val); } + { JSValue val = JS_GetPropertyNumber(js, v,i); v4.e[i] = js2number(js, val); JS_FreeValue(js,val); } return v4; } @@ -141,7 +141,7 @@ double arr_vec_length(JSContext *js,JSValue v) double sum = 0; for (int i = 0; i < len; i++) - { JSValue val = JS_GetPropertyUint32(js, v, i); double num = js2number(js, val); JS_FreeValue(js,val); sum += pow(num, 2); } + { JSValue val = JS_GetPropertyNumber(js, v, i); double num = js2number(js, val); JS_FreeValue(js,val); sum += pow(num, 2); } return sqrt(sum); } @@ -155,7 +155,7 @@ JSValue vec42js(JSContext *js, HMM_Vec4 v) { JSValue array = JS_NewArray(js); for (int i = 0; i < 4; i++) - JS_SetPropertyUint32(js, array,i,number2js(js,v.e[i])); + JS_SetPropertyNumber(js, array,i,number2js(js,v.e[i])); return array; } @@ -165,7 +165,7 @@ HMM_Vec2 *js2cpvec2arr(JSContext *js,JSValue v) { arrsetlen(arr,n); for (int i = 0; i < n; i++) { - JSValue ii = JS_GetPropertyUint32(js,v,i); + JSValue ii = JS_GetPropertyNumber(js,v,i); arr[i] = js2vec2(js,ii); JS_FreeValue(js,ii); } @@ -193,7 +193,7 @@ rect js2rect(JSContext *js,JSValue v) { static JSValue floats2array(JSContext *js, float *vals, size_t len) { JSValue arr = JS_NewArray(js); for (size_t i = 0; i < len; i++) { - JS_SetPropertyUint32(js, arr, i, number2js(js, vals[i])); + JS_SetPropertyNumber(js, arr, i, number2js(js, vals[i])); } return arr; } @@ -211,15 +211,15 @@ lrtb js2lrtb(JSContext *js, JSValue v) JSValue vec22js(JSContext *js,HMM_Vec2 v) { JSValue array = JS_NewArray(js); - JS_SetPropertyUint32(js, array,0,number2js(js,v.x)); - JS_SetPropertyUint32(js, array,1,number2js(js,v.y)); + JS_SetPropertyNumber(js, array,0,number2js(js,v.x)); + JS_SetPropertyNumber(js, array,1,number2js(js,v.y)); return array; } JSValue vecarr2js(JSContext *js,HMM_Vec2 *points, int n) { JSValue array = JS_NewArray(js); for (int i = 0; i < n; i++) - JS_SetPropertyUint32(js, array,i,vec22js(js,points[i])); + JS_SetPropertyNumber(js, array,i,vec22js(js,points[i])); return array; } diff --git a/rasterize.cm b/rasterize.cm index d85f3621..32f1d6f8 100644 --- a/rasterize.cm +++ b/rasterize.cm @@ -7,88 +7,106 @@ var math = use('math') var rasterize = {} -function within_wedge(dx, dy, start, end, full_circle) { - if (full_circle) return true +function within_wedge(dx, dy, wedge) { + if (wedge.full) return true var ang = math.arc_tangent(dy, dx) if (ang < 0) ang += pi * 2 var t = ang / (pi * 2) - if (start <= end) return t >= start && t <= end - return t >= start || t <= end + if (wedge.start <= wedge.end) return t >= wedge.start && t <= wedge.end + return t >= wedge.start || t <= wedge.end } rasterize.ellipse = function ellipse(pos, radii, opt) { - opt = opt || {} + var _opt = opt || {} var rx = radii[0], ry = radii[1] if (rx <= 0 || ry <= 0) return [] var cx = pos[0], cy = pos[1] - var raw_start = opt.start || 0 - var raw_end = opt.end || 1 + var raw_start = _opt.start || 0 + var raw_end = _opt.end || 1 var full_circle = abs(raw_end - raw_start) >= 1 - 1e-9 var start = (raw_start % 1 + 1) % 1 var end = (raw_end % 1 + 1) % 1 - var thickness = max(1, opt.thickness || 1) + var thickness = max(1, _opt.thickness || 1) + var wedge_info = {start: start, end: end, full: full_circle} var rx_i = rx - thickness, ry_i = ry - thickness var hole = (rx_i > 0 && ry_i > 0) - if (!hole && thickness == 1) { - var points = [] - var rx_sq = rx * rx, ry_sq = ry * ry - var two_rx_sq = rx_sq << 1, two_ry_sq = ry_sq << 1 - var x = 0, y = ry, px = 0, py = two_rx_sq * y - var p = ry_sq - rx_sq * ry + 0.25 * rx_sq + var points = [] + var rx_sq = rx * rx, ry_sq = ry * ry + var two_rx_sq = null + var two_ry_sq = null + var x = 0, y = 0, px = 0, py = 0 + var p = 0 - function add_pts(x, y) { - var pts = [ - [cx + x, cy + y], [cx - x, cy + y], - [cx + x, cy - y], [cx - x, cy - y] - ] - points = array(points, filter(pts, pt => within_wedge(pt[0]-cx, pt[1]-cy, start, end, full_circle))) - } + var add_pts = function(ax, ay) { + var pts = [ + [cx + ax, cy + ay], [cx - ax, cy + ay], + [cx + ax, cy - ay], [cx - ax, cy - ay] + ] + points = array(points, filter(pts, pt => within_wedge(pt[0]-cx, pt[1]-cy, wedge_info))) + } + + if (!hole && thickness == 1) { + two_rx_sq = rx_sq << 1 + two_ry_sq = ry_sq << 1 + x = 0 + y = ry + px = 0 + py = two_rx_sq * y + p = ry_sq - rx_sq * ry + 0.25 * rx_sq while (px < py) { add_pts(x, y) - ++x; px += two_ry_sq + x += 1; px += two_ry_sq if (p < 0) p += ry_sq + px - else { --y; py -= two_rx_sq; p += ry_sq + px - py } + else { y -= 1; py -= two_rx_sq; p += ry_sq + px - py } } p = ry_sq*(x+.5)*(x+.5) + rx_sq*(y-1)*(y-1) - rx_sq*ry_sq while (y >= 0) { add_pts(x, y) - --y; py -= two_rx_sq + y -= 1; py -= two_rx_sq if (p > 0) p += rx_sq - py - else { ++x; px += two_ry_sq; p += rx_sq - py + px } + else { x += 1; px += two_ry_sq; p += rx_sq - py + px } } return {type: 'points', data: points} } var strips = [] - var rx_sq = rx * rx, ry_sq = ry * ry var rx_i_sq = rx_i * rx_i, ry_i_sq = ry_i * ry_i + var dy = -ry + var yy = 0 + var x_out = 0 + var y_screen = 0 + var x_in = 0 + var run_start = null + var dx = 0 + var last = false + var next_in_ring = false - for (var dy = -ry; dy <= ry; ++dy) { - var yy = dy * dy - var x_out = floor(rx * math.sqrt(1 - yy / ry_sq)) - var y_screen = cy + dy + for (dy = -ry; dy <= ry; ++dy) { + yy = dy * dy + x_out = floor(rx * math.sqrt(1 - yy / ry_sq)) + y_screen = cy + dy - var x_in = hole ? floor(rx_i * math.sqrt(1 - yy / ry_i_sq)) : -1 + x_in = hole ? floor(rx_i * math.sqrt(1 - yy / ry_i_sq)) : -1 - var run_start = null - for (var dx = -x_out; dx <= x_out; ++dx) { + run_start = null + for (dx = -x_out; dx <= x_out; ++dx) { if (hole && abs(dx) <= x_in) { run_start = null; continue } - if (!within_wedge(dx, dy, start, end, full_circle)) { run_start = null; continue } + if (!within_wedge(dx, dy, wedge_info)) { run_start = null; continue } if (run_start == null) run_start = cx + dx - var last = (dx == x_out) - var next_in_ring = + last = (dx == x_out) + next_in_ring = !last && !(hole && abs(dx+1) <= x_in) && - within_wedge(dx+1, dy, start, end, full_circle) + within_wedge(dx+1, dy, wedge_info) if (last || !next_in_ring) { push(strips, { @@ -134,18 +152,18 @@ rasterize.outline_rect = function outline_rect(rect, thickness) { } rasterize.round_rect = function round_rect(rect, radius, thickness) { - thickness = thickness || 1 - - if (thickness <= 0) { + var _thickness = thickness || 1 + + if (_thickness <= 0) { return rasterize.fill_round_rect(rect, radius) } - radius = min(radius, rect.width >> 1, rect.height >> 1) + var _radius = min(radius, rect.width >> 1, rect.height >> 1) - if ((thickness << 1) >= rect.width || - (thickness << 1) >= rect.height || - thickness >= radius) { - return rasterize.fill_round_rect(rect, radius) + if ((_thickness << 1) >= rect.width || + (_thickness << 1) >= rect.height || + _thickness >= _radius) { + return rasterize.fill_round_rect(rect, _radius) } var x0 = rect.x, @@ -153,27 +171,32 @@ rasterize.round_rect = function round_rect(rect, radius, thickness) { x1 = rect.x + rect.width - 1, y1 = rect.y + rect.height - 1 - var cx_l = x0 + radius, cx_r = x1 - radius - var cy_t = y0 + radius, cy_b = y1 - radius - var r_out = radius - var r_in = radius - thickness + var cx_l = x0 + _radius, cx_r = x1 - _radius + var cy_t = y0 + _radius, cy_b = y1 - _radius + var r_out = _radius + var r_in = _radius - _thickness var rects = [ - { x:x0 + radius, y:y0, width:rect.width - (radius << 1), height:thickness }, - { x:x0 + radius, y:y1 - thickness + 1, width:rect.width - (radius << 1), height:thickness }, - { x:x0, y:y0 + radius, width:thickness, height:rect.height - (radius << 1) }, - { x:x1 - thickness + 1, y:y0 + radius, width:thickness, height:rect.height - (radius << 1) } + { x:x0 + _radius, y:y0, width:rect.width - (_radius << 1), height:_thickness }, + { x:x0 + _radius, y:y1 - _thickness + 1, width:rect.width - (_radius << 1), height:_thickness }, + { x:x0, y:y0 + _radius, width:_thickness, height:rect.height - (_radius << 1) }, + { x:x1 - _thickness + 1, y:y0 + _radius, width:_thickness, height:rect.height - (_radius << 1) } ] var strips = [] + var dy = 0 + var dy_sq = 0 + var dx_out = 0 + var dx_in = 0 + var w = 0 - for (var dy = 0; dy < radius; ++dy) { - var dy_sq = dy * dy - var dx_out = floor(math.sqrt(r_out * r_out - dy_sq)) - var dx_in = (r_in > 0 && dy < r_in) - ? floor(math.sqrt(r_in * r_in - dy_sq)) - : -1 - var w = dx_out - dx_in + for (dy = 0; dy < _radius; ++dy) { + dy_sq = dy * dy + dx_out = floor(math.sqrt(r_out * r_out - dy_sq)) + dx_in = (r_in > 0 && dy < r_in) + ? floor(math.sqrt(r_in * r_in - dy_sq)) + : -1 + w = dx_out - dx_in if (w <= 0) continue push(strips, @@ -188,7 +211,7 @@ rasterize.round_rect = function round_rect(rect, radius, thickness) { } rasterize.fill_round_rect = function fill_round_rect(rect, radius) { - radius = min(radius, rect.width >> 1, rect.height >> 1) + var _radius = min(radius, rect.width >> 1, rect.height >> 1) var x0 = rect.x, y0 = rect.y, @@ -196,20 +219,23 @@ rasterize.fill_round_rect = function fill_round_rect(rect, radius) { y1 = rect.y + rect.height - 1 var rects = [ - { x:x0 + radius, y:y0, width:rect.width - (radius << 1), height:rect.height }, - { x:x0, y:y0 + radius, width:radius, height:rect.height - (radius << 1) }, - { x:x1 - radius + 1, y:y0 + radius, width:radius, height:rect.height - (radius << 1) } + { x:x0 + _radius, y:y0, width:rect.width - (_radius << 1), height:rect.height }, + { x:x0, y:y0 + _radius, width:_radius, height:rect.height - (_radius << 1) }, + { x:x1 - _radius + 1, y:y0 + _radius, width:_radius, height:rect.height - (_radius << 1) } ] - var cx_l = x0 + radius, cx_r = x1 - radius - var cy_t = y0 + radius, cy_b = y1 - radius + var cx_l = x0 + _radius, cx_r = x1 - _radius + var cy_t = y0 + _radius, cy_b = y1 - _radius var caps = [] + var dy = 0 + var dx = 0 + var w = 0 - for (var dy = 0; dy < radius; ++dy) { - var dx = floor(math.sqrt(radius * radius - dy * dy)) - var w = (dx << 1) + 1 + for (dy = 0; dy < _radius; ++dy) { + dx = floor(math.sqrt(_radius * _radius - dy * dy)) + w = (dx << 1) + 1 - push(caps, + push(caps, { x:cx_l - dx, y:cy_t - dy, width:w, height:1 }, { x:cx_r - dx, y:cy_t - dy, width:w, height:1 }, { x:cx_l - dx, y:cy_b + dy, width:w, height:1 }, diff --git a/resources.cm b/resources.cm index 906f7cdf..276dc281 100644 --- a/resources.cm +++ b/resources.cm @@ -41,20 +41,22 @@ function isRecognizedExtension(ext) { return false } -function find_in_path(filename, exts = []) { +function find_in_path(filename, exts) { + var _exts = exts || [] if (!is_text(filename)) return null + var candidate = null if (search(filename, '.') != null) { - var candidate = filename // possibly need "/" ? + candidate = filename // possibly need "/" ? if (io.exists(candidate) && !io.is_directory(candidate)) return candidate return null } // Only check extensions if exts is provided and not empty - if (length(exts) > 0) { - var cand = null - arrfor(exts, function(ext) { - var candidate = filename + '.' + ext + var cand = null + if (length(_exts) > 0) { + arrfor(_exts, function(ext) { + candidate = filename + '.' + ext if (io.exists(candidate) && !io.is_directory(candidate)){ cand = candidate return true @@ -63,7 +65,7 @@ function find_in_path(filename, exts = []) { if (cand != null) return cand } else { // Fallback to extensionless file only if no extensions are specified - var candidate = filename + candidate = filename if (io.exists(candidate) && !io.is_directory(candidate)) return candidate } return null @@ -95,12 +97,13 @@ Resources.find_font = hashify(function(file) { function read_ignore(dir) { var path = dir + '/.prosperonignore' var patterns = [] + var lines = null if (io.exists(path)) { - var lines = array(io.slurp(path), '\n') + lines = array(io.slurp(path), '\n') arrfor(lines, function(line) { - line = trim(line) - if (!line || starts_with(line, '#')) return - push(patterns, line) + var trimmed = trim(line) + if (!trimmed || starts_with(trimmed, '#')) return + push(patterns, trimmed) }) } return patterns @@ -108,20 +111,24 @@ function read_ignore(dir) { // Return a list of recognized files in the directory (and subdirectories), // skipping those matched by .prosperonignore. Directory paths are skipped. -Resources.getAllFiles = function(dir = "") { - var patterns = read_ignore(dir) - var all = io.globfs(patterns, dir) +Resources.getAllFiles = function(dir) { + var _dir = dir || "" + var patterns = read_ignore(_dir) + var all = io.globfs(patterns, _dir) var results = [] arrfor(all, function(f) { - var fullPath = dir + '/' + f - try { + var fullPath = _dir + '/' + f + var _stat = function() { var st = io.stat(fullPath) // skip directories (filesize=0) or unrecognized extension if (!st.filesize) return var ext = getExtension(f) if (!isRecognizedExtension(ext)) return push(results, fullPath) - } catch(e) {} + } disruption { + // skip files that can't be stat'd + } + _stat() }) return results } diff --git a/sdl_gpu.cm b/sdl_gpu.cm index 329f0ed5..9aca74e5 100644 --- a/sdl_gpu.cm +++ b/sdl_gpu.cm @@ -60,12 +60,12 @@ var _target_pool = {} // ======================================================================== sdl_gpu.init = function(opts) { - opts = opts || {} - _window_width = opts.width || 1280 - _window_height = opts.height || 720 - + var local_opts = opts || {} + _window_width = local_opts.width || 1280 + _window_height = local_opts.height || 720 + _window = video.window({ - title: opts.title || "Prosperon", + title: local_opts.title || "Prosperon", width: _window_width, height: _window_height, resizable: true @@ -697,35 +697,26 @@ function _create_gpu_texture(w, h, pixels) { function _load_image_file(path) { var bytes = io.slurp(path) - var decoded + var decoded = null if (!bytes) return null - + var ext = lower(pop(array(path, '.'))) var surface = null - - switch (ext) { - case 'png': - case 'jpg': - case 'jpeg': - case 'bmp': - surface = png.decode(bytes) - break - case 'qoi': - surface = qoi.decode(bytes) - break - case 'gif': - decoded = gif.decode(bytes) - if (decoded && decoded.frames && length(decoded.frames) > 0) { - surface = decoded.frames[0] - } - break - case 'ase': - case 'aseprite': - decoded = aseprite.decode(bytes) - if (decoded && decoded.frames && length(decoded.frames) > 0) { - surface = decoded.frames[0] - } - break + + if (ext == 'png' || ext == 'jpg' || ext == 'jpeg' || ext == 'bmp') { + surface = png.decode(bytes) + } else if (ext == 'qoi') { + surface = qoi.decode(bytes) + } else if (ext == 'gif') { + decoded = gif.decode(bytes) + if (decoded && decoded.frames && length(decoded.frames) > 0) { + surface = decoded.frames[0] + } + } else if (ext == 'ase' || ext == 'aseprite') { + decoded = aseprite.decode(bytes) + if (decoded && decoded.frames && length(decoded.frames) > 0) { + surface = decoded.frames[0] + } } return surface @@ -771,28 +762,29 @@ sdl_gpu.get_texture_info = function(path) { sdl_gpu.get_or_create_target = function(width, height, key) { // Clamp dimensions to minimum 1x1 to prevent GPU errors - if (!width || width < 1) width = 1 - if (!height || height < 1) height = 1 - - var pool_key = `${width}x${height}` + var w = (!width || width < 1) ? 1 : width + var h = (!height || height < 1) ? 1 : height + + var pool_key = `${w}x${h}` if (!_target_pool[pool_key]) _target_pool[pool_key] = [] // Reuse from pool if available + var pool = _target_pool[pool_key] + var idx = null + // 1. Check if a target with this exact key already exists if (key) { - var pool = _target_pool[pool_key] - var idx = find(pool, function(t) { return t.key == key }) + idx = find(pool, function(t) { return t.key == key }) if (idx != null) { pool[idx].in_use = true return pool[idx] } } - + // 2. Otherwise prefer most recently used (LIFO) or just first available - var pool = _target_pool[pool_key] - var idx = find(pool, function(t) { return !t.in_use }) + idx = find(pool, function(t) { return !t.in_use }) if (idx != null) { pool[idx].in_use = true pool[idx].key = key @@ -801,8 +793,8 @@ sdl_gpu.get_or_create_target = function(width, height, key) { // Create new render target texture var tex =gpu_mod.texture(_gpu, { - width: width, - height: height, + width: w, + height: h, format: _swapchain_format, type: "2d", layers: 1, @@ -810,14 +802,14 @@ sdl_gpu.get_or_create_target = function(width, height, key) { sampler: true, color_target: true }) - - tex.width = width - tex.height = height - + + tex.width = w + tex.height = h + var target = { texture: tex, - width: width, - height: height, + width: w, + height: h, in_use: true, key: key } @@ -851,57 +843,84 @@ function _build_sprite_vertices(sprites, camera) { var vertex_count = 0 var white = {r: 1, g: 1, b: 1, a: 1} - - for(var i = 0; i < length(sprites); i++) { - var s = sprites[i] - var px = s.pos.x - var py = s.pos.y - var w = s.width || 1 - var h = s.height || 1 - var ax = s.anchor_x || 0 - var ay = s.anchor_y || 0 - var c = s.color || white - + + var i = 0 + var s = null + var px = 0 + var py = 0 + var w = 0 + var h = 0 + var ax = 0 + var ay = 0 + var c = null + var tint = null + var opacity = 0 + var final_r = 0 + var final_g = 0 + var final_b = 0 + var final_a = 0 + var x = 0 + var y = 0 + var u0 = 0 + var v0 = 0 + var u1 = 0 + var v1 = 0 + var uv = null + var uv_off = null + var uv_scale = null + var flip = null + var tmp = 0 + + for(i = 0; i < length(sprites); i++) { + s = sprites[i] + px = s.pos.x + py = s.pos.y + w = s.width || 1 + h = s.height || 1 + ax = s.anchor_x || 0 + ay = s.anchor_y || 0 + c = s.color || white + // Apply tint and opacity - var tint = s.tint || white - var opacity = s.opacity != null ? s.opacity : 1 - var final_r = c.r * tint.r - var final_g = c.g * tint.g - var final_b = c.b * tint.b - var final_a = c.a * (tint.a != null ? tint.a : 1) * opacity - + tint = s.tint || white + opacity = s.opacity != null ? s.opacity : 1 + final_r = c.r * tint.r + final_g = c.g * tint.g + final_b = c.b * tint.b + final_a = c.a * (tint.a != null ? tint.a : 1) * opacity + // Apply anchor - var x = px - w * ax - var y = py - h * ay - + x = px - w * ax + y = py - h * ay + // UV coordinates (handle sprite rect if present) - var u0 = s.uv_rect ? s.uv_rect.x : 0 - var v0 = s.uv_rect ? s.uv_rect.y : 0 - var u1 = s.uv_rect ? (s.uv_rect.x + s.uv_rect.width) : 1 - var v1 = s.uv_rect ? (s.uv_rect.y + s.uv_rect.height) : 1 - + u0 = s.uv_rect ? s.uv_rect.x : 0 + v0 = s.uv_rect ? s.uv_rect.y : 0 + u1 = s.uv_rect ? (s.uv_rect.x + s.uv_rect.width) : 1 + v1 = s.uv_rect ? (s.uv_rect.y + s.uv_rect.height) : 1 + // Apply UV transform (offset, scale, rotate) - var uv = s.uv + uv = s.uv if (uv) { - var uv_off = uv.offset || {x: 0, y: 0} - var uv_scale = uv.scale || {x: 1, y: 1} + uv_off = uv.offset || {x: 0, y: 0} + uv_scale = uv.scale || {x: 1, y: 1} // Apply scale and offset to UVs u0 = u0 * uv_scale.x + uv_off.x v0 = v0 * uv_scale.y + uv_off.y u1 = u1 * uv_scale.x + uv_off.x v1 = v1 * uv_scale.y + uv_off.y } - + // Apply flip - var flip = s.flip + flip = s.flip if (flip) { if (flip.x) { - var tmp = u0 + tmp = u0 u0 = u1 u1 = tmp } if (flip.y) { - var tmp = v0 + tmp = v0 v0 = v1 v1 = tmp } @@ -1026,33 +1045,34 @@ function _build_fullscreen_quad(dst_rect, target_width, target_height) { // MATRIX BUILDING // ======================================================================== -function _build_ortho_matrix(left, right, bottom, top, near, far) { +function _build_ortho_matrix(bounds, depth) { var data = blob_mod(64) var m = [] - - m[0] = 2 / (right - left) + + m[0] = 2 / (bounds.right - bounds.left) m[1] = 0 m[2] = 0 m[3] = 0 - + m[4] = 0 - m[5] = 2 / (top - bottom) + m[5] = 2 / (bounds.top - bounds.bottom) m[6] = 0 m[7] = 0 - + m[8] = 0 m[9] = 0 - m[10] = -2 / (far - near) + m[10] = -2 / (depth.far - depth.near) m[11] = 0 - - m[12] = -(right + left) / (right - left) - m[13] = -(top + bottom) / (top - bottom) - m[14] = -(far + near) / (far - near) + + m[12] = -(bounds.right + bounds.left) / (bounds.right - bounds.left) + m[13] = -(bounds.top + bounds.bottom) / (bounds.top - bounds.bottom) + m[14] = -(depth.far + depth.near) / (depth.far - depth.near) m[15] = 1 - - for (var i = 0; i < 16; i++) + + var i = 0 + for (i = 0; i < 16; i++) data.wf(m[i]) - + return stone(data) } @@ -1067,20 +1087,21 @@ function _build_camera_matrix(camera, target_width, target_height) { var bottom = pos.y - cam_height * anchor.y var top = pos.y + cam_height * (1 - anchor.y) - return _build_ortho_matrix(left, right, bottom, top, -1, 1) + return _build_ortho_matrix({left: left, right: right, bottom: bottom, top: top}, {near: -1, far: 1}) } // ======================================================================== // GRAPH EXECUTION // ======================================================================== var ex = 0 -sdl_gpu.execute_graph = function(graph, window_size, dbg = false) { +sdl_gpu.execute_graph = function(graph, window_size, dbg) { + var local_dbg = dbg || false _window_width = window_size.width _window_height = window_size.height - + // Execute graph to get all commands var result = graph.execute(this) - if (dbg) { + if (local_dbg) { log.console(result) return } @@ -1097,11 +1118,12 @@ sdl_gpu.execute_graph = function(graph, window_size, dbg = false) { } // Execute commands directly (from compositor) -sdl_gpu.execute_commands = function(commands, window_size, dbg = false) { +sdl_gpu.execute_commands = function(commands, window_size, dbg) { + var local_dbg = dbg || false _window_width = window_size.width _window_height = window_size.height - - if (dbg) { + + if (local_dbg) { log.console(commands) return } @@ -1138,8 +1160,8 @@ function _execute_commands(commands, window_size) { var current_target = null var current_camera = null var pending_draws = [] - var target - + var target = null + // Cache swapchain texture for the duration of this command buffer var _swapchain_tex = null function get_swapchain_tex() { @@ -1148,215 +1170,192 @@ function _execute_commands(commands, window_size) { return _swapchain_tex } + var clear = null + var swap_tex = null + var imgui_mod = null + arrfor(commands, function(cmd) { - switch (cmd.cmd) { - case 'begin_render': - // Flush pending draws - if (current_pass && length(pending_draws) > 0) { - _flush_draws(cmd_buffer, current_pass, pending_draws, current_camera, current_target) - pending_draws = [] - } - - // End previous pass - if (current_pass) { - current_pass.end() - current_pass = null - } - - // Start new pass - target = cmd.target - var clear = cmd.clear - - if (target == 'screen') { - var swap_tex = get_swapchain_tex() - if (swap_tex) { - current_pass = cmd_buffer.render_pass({ - color_targets: [{ - texture: swap_tex, - load: clear ? "clear" : "load", - store: "store", - clear_color: clear ? {r: clear.r, g: clear.g, b: clear.b, a: clear.a} : {r: 0, g: 0, b: 0, a: 0} - }] - }) - current_target = {texture: swap_tex, width: swap_tex.width, height: swap_tex.height} - } else { - log.console("sdl_gpu: Failed to acquire swapchain texture") - current_pass = null - current_target = window_size - } - } else { + if (cmd.cmd == 'begin_render') { + // Flush pending draws + if (current_pass && length(pending_draws) > 0) { + _flush_draws({cmd_buffer: cmd_buffer, pass: current_pass, camera: current_camera, target: current_target}, pending_draws) + pending_draws = [] + } + + // End previous pass + if (current_pass) { + current_pass.end() + current_pass = null + } + + // Start new pass + target = cmd.target + clear = cmd.clear + + if (target == 'screen') { + swap_tex = get_swapchain_tex() + if (swap_tex) { current_pass = cmd_buffer.render_pass({ color_targets: [{ - texture: target.texture, + texture: swap_tex, load: clear ? "clear" : "load", store: "store", clear_color: clear ? {r: clear.r, g: clear.g, b: clear.b, a: clear.a} : {r: 0, g: 0, b: 0, a: 0} }] }) - current_target = target - } - break - - case 'set_camera': - current_camera = cmd.camera - break - - case 'draw_batch': - push(pending_draws, cmd) - break - - case 'draw_text': - push(pending_draws, cmd) - break - - case 'draw_texture_ref': - push(pending_draws, cmd) - break - - case 'draw_shape': - push(pending_draws, cmd) - break - - case 'draw_mesh2d': - push(pending_draws, cmd) - break - - case 'blit': - // Flush pending draws first - if (current_pass && length(pending_draws) > 0) { - _flush_draws(cmd_buffer, current_pass, pending_draws, current_camera, current_target) - pending_draws = [] - } - - // End current pass - SDL blit works outside render passes - if (current_pass) { - current_pass.end() + current_target = {texture: swap_tex, width: swap_tex.width, height: swap_tex.height} + } else { + log.console("sdl_gpu: Failed to acquire swapchain texture") current_pass = null + current_target = window_size } - - _do_blit(cmd_buffer, cmd, current_target, get_swapchain_tex) - break - - case 'apply_mask': - // Flush pending draws first - if (current_pass && length(pending_draws) > 0) { - _flush_draws(cmd_buffer, current_pass, pending_draws, current_camera, current_target) - pending_draws = [] - } - - // End current pass - mask works as blit outside render pass - if (current_pass) { - current_pass.end() - current_pass = null - } - - _do_mask(cmd_buffer, cmd) - break - - case 'shader_pass': - // Flush pending draws first - if (current_pass && length(pending_draws) > 0) { - _flush_draws(cmd_buffer, current_pass, pending_draws, current_camera, current_target) - pending_draws = [] - } - - // End current pass - shader passes need their own render pass - if (current_pass) { - current_pass.end() - current_pass = null - } - - _do_shader_pass(cmd_buffer, cmd, get_swapchain_tex) - break - - case 'composite_textures': - // Flush pending draws first - if (current_pass && length(pending_draws) > 0) { - _flush_draws(cmd_buffer, current_pass, pending_draws, current_camera, current_target) - pending_draws = [] - } - - // End current pass - if (current_pass) { - current_pass.end() - current_pass = null - } - - _do_composite(cmd_buffer, cmd, window_size) - break - - case 'end_render': - // Flush pending draws - if (current_pass && length(pending_draws) > 0) { - _flush_draws(cmd_buffer, current_pass, pending_draws, current_camera, current_target) - pending_draws = [] - } - - if (current_pass) { - current_pass.end() - current_pass = null - } - break + } else { + current_pass = cmd_buffer.render_pass({ + color_targets: [{ + texture: target.texture, + load: clear ? "clear" : "load", + store: "store", + clear_color: clear ? {r: clear.r, g: clear.g, b: clear.b, a: clear.a} : {r: 0, g: 0, b: 0, a: 0} + }] + }) + current_target = target + } - case 'imgui': - // Flush pending draws first - if (current_pass && length(pending_draws) > 0) { - _flush_draws(cmd_buffer, current_pass, pending_draws, current_camera, current_target) - pending_draws = [] - } - - // ImGui needs to be outside a render pass for prepare, but inside for endframe - if (current_pass) { - current_pass.end() - current_pass = null - } - - var imgui_mod = use('imgui') - if (cmd.draw) { - cmd.draw(imgui_mod) - } - imgui_mod.prepare(cmd_buffer) - - // Restart pass to the same target for rendering - target = cmd.target - var swap_tex = null - if (target == 'screen') { - swap_tex = get_swapchain_tex() - if (swap_tex) { - current_pass = cmd_buffer.render_pass({ - color_targets: [{ - texture: swap_tex, - load: "load", - store: "store" - }] - }) - } - } else if (target && target.texture) { + } else if (cmd.cmd == 'set_camera') { + current_camera = cmd.camera + + } else if (cmd.cmd == 'draw_batch' || cmd.cmd == 'draw_text' || cmd.cmd == 'draw_texture_ref' || cmd.cmd == 'draw_shape' || cmd.cmd == 'draw_mesh2d') { + push(pending_draws, cmd) + + } else if (cmd.cmd == 'blit') { + // Flush pending draws first + if (current_pass && length(pending_draws) > 0) { + _flush_draws({cmd_buffer: cmd_buffer, pass: current_pass, camera: current_camera, target: current_target}, pending_draws) + pending_draws = [] + } + + // End current pass - SDL blit works outside render passes + if (current_pass) { + current_pass.end() + current_pass = null + } + + _do_blit(cmd_buffer, cmd, current_target, get_swapchain_tex) + + } else if (cmd.cmd == 'apply_mask') { + // Flush pending draws first + if (current_pass && length(pending_draws) > 0) { + _flush_draws({cmd_buffer: cmd_buffer, pass: current_pass, camera: current_camera, target: current_target}, pending_draws) + pending_draws = [] + } + + // End current pass - mask works as blit outside render pass + if (current_pass) { + current_pass.end() + current_pass = null + } + + _do_mask(cmd_buffer, cmd) + + } else if (cmd.cmd == 'shader_pass') { + // Flush pending draws first + if (current_pass && length(pending_draws) > 0) { + _flush_draws({cmd_buffer: cmd_buffer, pass: current_pass, camera: current_camera, target: current_target}, pending_draws) + pending_draws = [] + } + + // End current pass - shader passes need their own render pass + if (current_pass) { + current_pass.end() + current_pass = null + } + + _do_shader_pass(cmd_buffer, cmd, get_swapchain_tex) + + } else if (cmd.cmd == 'composite_textures') { + // Flush pending draws first + if (current_pass && length(pending_draws) > 0) { + _flush_draws({cmd_buffer: cmd_buffer, pass: current_pass, camera: current_camera, target: current_target}, pending_draws) + pending_draws = [] + } + + // End current pass + if (current_pass) { + current_pass.end() + current_pass = null + } + + _do_composite(cmd_buffer, cmd, window_size) + + } else if (cmd.cmd == 'end_render') { + // Flush pending draws + if (current_pass && length(pending_draws) > 0) { + _flush_draws({cmd_buffer: cmd_buffer, pass: current_pass, camera: current_camera, target: current_target}, pending_draws) + pending_draws = [] + } + + if (current_pass) { + current_pass.end() + current_pass = null + } + + } else if (cmd.cmd == 'imgui') { + // Flush pending draws first + if (current_pass && length(pending_draws) > 0) { + _flush_draws({cmd_buffer: cmd_buffer, pass: current_pass, camera: current_camera, target: current_target}, pending_draws) + pending_draws = [] + } + + // ImGui needs to be outside a render pass for prepare, but inside for endframe + if (current_pass) { + current_pass.end() + current_pass = null + } + + imgui_mod = use('imgui') + if (cmd.draw) { + cmd.draw(imgui_mod) + } + imgui_mod.prepare(cmd_buffer) + + // Restart pass to the same target for rendering + target = cmd.target + swap_tex = null + if (target == 'screen') { + swap_tex = get_swapchain_tex() + if (swap_tex) { current_pass = cmd_buffer.render_pass({ color_targets: [{ - texture: target.texture, + texture: swap_tex, load: "load", store: "store" }] }) } - - if (current_pass) { - imgui_mod.endframe(cmd_buffer, current_pass) - current_pass.end() - current_pass = null - } - break - - case 'present': - // Submit command buffer - break + } else if (target && target.texture) { + current_pass = cmd_buffer.render_pass({ + color_targets: [{ + texture: target.texture, + load: "load", + store: "store" + }] + }) + } + + if (current_pass) { + imgui_mod.endframe(cmd_buffer, current_pass) + current_pass.end() + current_pass = null + } + + } else if (cmd.cmd == 'present') { + // Submit command buffer } }) // Final flush if (current_pass && length(pending_draws) > 0) { - _flush_draws(cmd_buffer, current_pass, pending_draws, current_camera, current_target) + _flush_draws({cmd_buffer: cmd_buffer, pass: current_pass, camera: current_camera, target: current_target}, pending_draws) } if (current_pass) { @@ -1366,32 +1365,35 @@ function _execute_commands(commands, window_size) { cmd_buffer.submit() } -function _flush_draws(cmd_buffer, pass, draws, camera, target) { +function _flush_draws(ctx, draws) { var current_batch = null - + // Iterate draws preserving order arrfor(draws, function(draw) { + var tex_path = null + var blend = null + var sampler = null if (draw.cmd == 'draw_batch') { // Sprite batch handling - var tex_path = draw.texture || '_white' - var blend = draw.material ? draw.material.blend : 'alpha' - var sampler = draw.material ? draw.material.sampler : 'nearest' - + tex_path = draw.texture || '_white' + blend = draw.material ? draw.material.blend : 'alpha' + sampler = draw.material ? draw.material.sampler : 'nearest' + // Check if we can append to current batch - if (current_batch && - current_batch.type == 'sprites' && - current_batch.texture_path == tex_path && + if (current_batch && + current_batch.type == 'sprites' && + current_batch.texture_path == tex_path && current_batch.blend == blend && current_batch.sampler == sampler) { - + // Append sprites if (draw.geometry && draw.geometry.sprites) { current_batch.sprites = array(current_batch.sprites, draw.geometry.sprites) } } else { // Flush current batch - if (current_batch) _render_batch(cmd_buffer, pass, current_batch, camera, target) - + if (current_batch) _render_batch(ctx, current_batch) + // Start new sprite batch current_batch = { type: 'sprites', @@ -1400,101 +1402,115 @@ function _flush_draws(cmd_buffer, pass, draws, camera, target) { sampler: sampler, sprites: [] } - + if (draw.geometry && draw.geometry.sprites) current_batch.sprites = array(current_batch.sprites, draw.geometry.sprites) } } else if (draw.cmd == 'draw_text') { // Flush current batch - if (current_batch) _render_batch(cmd_buffer, pass, current_batch, camera, target) + if (current_batch) _render_batch(ctx, current_batch) current_batch = null // Render text immediately - _render_text(cmd_buffer, pass, draw.drawable, camera, target) + _render_text(ctx, draw.drawable) } else if (draw.cmd == 'draw_texture_ref') { // Flush current batch - if (current_batch) _render_batch(cmd_buffer, pass, current_batch, camera, target) + if (current_batch) _render_batch(ctx, current_batch) current_batch = null // Render pre-rendered effect texture - _render_texture_ref(cmd_buffer, pass, draw.drawable, camera, target) + _render_texture_ref(ctx, draw.drawable) } else if (draw.cmd == 'draw_shape') { // Flush current batch - if (current_batch) _render_batch(cmd_buffer, pass, current_batch, camera, target) + if (current_batch) _render_batch(ctx, current_batch) current_batch = null // Render shape immediately - _render_shape(cmd_buffer, pass, draw.drawable, camera, target) + _render_shape(ctx, draw.drawable) } else if (draw.cmd == 'draw_mesh2d') { // Flush current batch - if (current_batch) _render_batch(cmd_buffer, pass, current_batch, camera, target) + if (current_batch) _render_batch(ctx, current_batch) current_batch = null // Render mesh2d batch - _render_mesh2d(cmd_buffer, pass, draw, camera, target) + _render_mesh2d(ctx, draw) } }) // Flush final batch - if (current_batch) _render_batch(cmd_buffer, pass, current_batch, camera, target) + if (current_batch) _render_batch(ctx, current_batch) } -function _render_batch(cmd_buffer, pass, batch, camera, target) { +function _render_batch(ctx, batch) { + var tex = null + var geom = null + var vb_size = 0 + var ib_size = 0 + var vb = null + var ib = null + var vb_transfer = null + var ib_transfer = null + var copy_cmd = null + var copy = null + var proj = null + var pipeline = null + var sampler = null + if (batch.type == 'sprites') { if (length(batch.sprites) == 0) return - - var tex = batch.texture_path == '_white' ? _white_texture : sdl_gpu.get_texture(batch.texture_path) - var geom = _build_sprite_vertices(batch.sprites, camera) - + + tex = batch.texture_path == '_white' ? _white_texture : sdl_gpu.get_texture(batch.texture_path) + geom = _build_sprite_vertices(batch.sprites, ctx.camera) + // Upload geometry - var vb_size = length(geom.vertices) / 8 - var ib_size = length(geom.indices) / 8 - - var vb =gpu_mod.buffer(_gpu, {size: vb_size, vertex: true}) - var ib =gpu_mod.buffer(_gpu, {size: ib_size, index: true}) - - var vb_transfer =gpu_mod.transfer_buffer(_gpu, {size: vb_size, usage: "upload"}) - var ib_transfer =gpu_mod.transfer_buffer(_gpu, {size: ib_size, usage: "upload"}) - + vb_size = length(geom.vertices) / 8 + ib_size = length(geom.indices) / 8 + + vb =gpu_mod.buffer(_gpu, {size: vb_size, vertex: true}) + ib =gpu_mod.buffer(_gpu, {size: ib_size, index: true}) + + vb_transfer =gpu_mod.transfer_buffer(_gpu, {size: vb_size, usage: "upload"}) + ib_transfer =gpu_mod.transfer_buffer(_gpu, {size: ib_size, usage: "upload"}) + vb_transfer.copy_blob(_gpu, geom.vertices) ib_transfer.copy_blob(_gpu, geom.indices) - - var copy_cmd = _gpu.acquire_cmd_buffer() - var copy = copy_cmd.copy_pass() + + copy_cmd = _gpu.acquire_cmd_buffer() + copy = copy_cmd.copy_pass() copy.upload_to_buffer({transfer_buffer: vb_transfer, offset: 0}, {buffer: vb, offset: 0, size: vb_size}, false) copy.upload_to_buffer({transfer_buffer: ib_transfer, offset: 0}, {buffer: ib, offset: 0, size: ib_size}, false) copy.end() copy_cmd.submit() - + // Build camera matrix - var proj = _build_camera_matrix(camera, target.width, target.height) - + proj = _build_camera_matrix(ctx.camera, ctx.target.width, ctx.target.height) + // Select pipeline - var pipeline = batch.blend == 'add' ? _pipelines.sprite_add : _pipelines.sprite_alpha - + pipeline = batch.blend == 'add' ? _pipelines.sprite_add : _pipelines.sprite_alpha + // Select sampler based on filter - var sampler = (batch.sampler == 'linear') ? _sampler_linear : _sampler_nearest - + sampler = (batch.sampler == 'linear') ? _sampler_linear : _sampler_nearest + // Draw - pass.bind_pipeline(pipeline) - pass.bind_vertex_buffers(0, [{buffer: vb, offset: 0}]) - pass.bind_index_buffer({buffer: ib, offset: 0}, 16) - pass.bind_fragment_samplers(0, [{texture: tex, sampler: sampler}]) - cmd_buffer.push_vertex_uniform_data(0, proj) - pass.draw_indexed(geom.index_count, 1, 0, 0, 0) + ctx.pass.bind_pipeline(pipeline) + ctx.pass.bind_vertex_buffers(0, [{buffer: vb, offset: 0}]) + ctx.pass.bind_index_buffer({buffer: ib, offset: 0}, 16) + ctx.pass.bind_fragment_samplers(0, [{texture: tex, sampler: sampler}]) + ctx.cmd_buffer.push_vertex_uniform_data(0, proj) + ctx.pass.draw_indexed(geom.index_count, 1, 0, 0, 0) } } // Render a pre-rendered texture from an effect group -function _render_texture_ref(cmd_buffer, pass, drawable, camera, target) { +function _render_texture_ref(ctx, drawable) { var tex_target = drawable.texture_target if (!tex_target) return // The texture_target is a compositor target reference - resolve it // It should have already been rendered to and we just need to blit it var pos = drawable.pos || {x: 0, y: 0} - var width = drawable.width || target.width - var height = drawable.height || target.height + var width = drawable.width || ctx.target.width + var height = drawable.height || ctx.target.height // Build a single sprite for the texture reference var sprites = [{ @@ -1506,7 +1522,7 @@ function _render_texture_ref(cmd_buffer, pass, drawable, camera, target) { color: {r: 1, g: 1, b: 1, a: 1} }] - var geom = _build_sprite_vertices(sprites, camera) + var geom = _build_sprite_vertices(sprites, ctx.camera) // Upload geometry var vb_size = length(geom.vertices) / 8 @@ -1529,7 +1545,7 @@ function _render_texture_ref(cmd_buffer, pass, drawable, camera, target) { copy_cmd.submit() // Build camera matrix - var proj = _build_camera_matrix(camera, target.width, target.height) + var proj = _build_camera_matrix(ctx.camera, ctx.target.width, ctx.target.height) // Select pipeline based on blend mode var blend = drawable.blend || 'over' @@ -1539,15 +1555,17 @@ function _render_texture_ref(cmd_buffer, pass, drawable, camera, target) { var tex = tex_target.texture || tex_target if (!tex) return - pass.bind_pipeline(pipeline) - pass.bind_vertex_buffers(0, [{buffer: vb, offset: 0}]) - pass.bind_index_buffer({buffer: ib, offset: 0}, 16) - pass.bind_fragment_samplers(0, [{texture: tex, sampler: _sampler_linear}]) - cmd_buffer.push_vertex_uniform_data(0, proj) - pass.draw_indexed(geom.index_count, 1, 0, 0, 0) + ctx.pass.bind_pipeline(pipeline) + ctx.pass.bind_vertex_buffers(0, [{buffer: vb, offset: 0}]) + ctx.pass.bind_index_buffer({buffer: ib, offset: 0}, 16) + ctx.pass.bind_fragment_samplers(0, [{texture: tex, sampler: _sampler_linear}]) + ctx.cmd_buffer.push_vertex_uniform_data(0, proj) + ctx.pass.draw_indexed(geom.index_count, 1, 0, 0, 0) } -function _render_shape(cmd_buffer, pass, drawable, camera, target) { +function _render_shape(ctx, drawable) { + var camera = ctx.camera + var target = ctx.target if (!_pipelines.shape2d) return var pos = drawable.pos || {x: 0, y: 0} @@ -1679,16 +1697,18 @@ function _render_shape(cmd_buffer, pass, drawable, camera, target) { u_data.wf(opacity) // opacity (4) u_data.wf(0) // _pad (4) - pass.bind_pipeline(_pipelines.shape2d) - pass.bind_vertex_buffers(0, [{buffer: vb, offset: 0}]) - pass.bind_index_buffer({buffer: ib, offset: 0}, 16) - pass.bind_fragment_samplers(0, [{texture: _white_texture, sampler: _sampler_linear}]) - cmd_buffer.push_vertex_uniform_data(0, proj) - cmd_buffer.push_fragment_uniform_data(0, stone(u_data)) - pass.draw_indexed(6, 1, 0, 0, 0) + ctx.pass.bind_pipeline(_pipelines.shape2d) + ctx.pass.bind_vertex_buffers(0, [{buffer: vb, offset: 0}]) + ctx.pass.bind_index_buffer({buffer: ib, offset: 0}, 16) + ctx.pass.bind_fragment_samplers(0, [{texture: _white_texture, sampler: _sampler_linear}]) + ctx.cmd_buffer.push_vertex_uniform_data(0, proj) + ctx.cmd_buffer.push_fragment_uniform_data(0, stone(u_data)) + ctx.pass.draw_indexed(6, 1, 0, 0, 0) } -function _render_mesh2d(cmd_buffer, pass, draw, camera, target) { +function _render_mesh2d(ctx, draw) { + var camera = ctx.camera + var target = ctx.target var meshes = draw.meshes || [] if (length(meshes) == 0) return @@ -1773,15 +1793,17 @@ function _render_mesh2d(cmd_buffer, pass, draw, camera, target) { var sampler = sampler_type == 'linear' ? _sampler_linear : _sampler_nearest // Draw - pass.bind_pipeline(pipeline) - pass.bind_vertex_buffers(0, [{buffer: vb, offset: 0}]) - pass.bind_index_buffer({buffer: ib, offset: 0}, 16) - pass.bind_fragment_samplers(0, [{texture: tex, sampler: sampler}]) - cmd_buffer.push_vertex_uniform_data(0, proj) - pass.draw_indexed(total_indices, 1, 0, 0, 0) + ctx.pass.bind_pipeline(pipeline) + ctx.pass.bind_vertex_buffers(0, [{buffer: vb, offset: 0}]) + ctx.pass.bind_index_buffer({buffer: ib, offset: 0}, 16) + ctx.pass.bind_fragment_samplers(0, [{texture: tex, sampler: sampler}]) + ctx.cmd_buffer.push_vertex_uniform_data(0, proj) + ctx.pass.draw_indexed(total_indices, 1, 0, 0, 0) } -function _render_text(cmd_buffer, pass, drawable, camera, target) { +function _render_text(ctx, drawable) { + var camera = ctx.camera + var target = ctx.target // Get font - support mode tag: 'bitmap', 'sdf', 'msdf' var font_path = drawable.font var size = drawable.size || 16 @@ -1797,9 +1819,10 @@ function _render_text(cmd_buffer, pass, drawable, camera, target) { // Handle anchor var ax = drawable.anchor_x || 0 var ay = drawable.anchor_y || 0 - + var dim = null + if (ax != 0 || ay != 0) { - var dim = font.text_size(drawable.text) + dim = font.text_size(drawable.text) if (dim) { text_pos.x -= dim.x * ax text_pos.y -= dim.y * ay @@ -1843,77 +1866,82 @@ function _render_text(cmd_buffer, pass, drawable, camera, target) { var is_sdf = (mode == 'sdf') var is_msdf = (mode == 'msdf') + var u_data = null + var outline_w = 0 + var oc = null + if (is_msdf && _pipelines.text_msdf) { - pass.bind_pipeline(_pipelines.text_msdf) - + ctx.pass.bind_pipeline(_pipelines.text_msdf) + // Build uniforms for MSDF // Struct: float outline_width, float sharpness, float2 _pad, float4 outline_color - var u_data = blob_mod(32) - + u_data = blob_mod(32) + // Convert outline_width from pixel-ish units to normalized SDF units // outline_width in drawable is in "visual" units, we need to normalize // A typical range is 0.0-0.3 in SDF units - var outline_w = drawable.outline_width || 0 + outline_w = drawable.outline_width || 0 if (outline_w > 0) outline_w = outline_w / 100.0 // Scale down from user units - + u_data.wf(outline_w) // outline_width u_data.wf(font.sharpness || 1.0) // sharpness from font u_data.wf(0) // _pad.x u_data.wf(0) // _pad.y - - var oc = drawable.outline_color || {r:0, g:0, b:0, a:1} + + oc = drawable.outline_color || {r:0, g:0, b:0, a:1} u_data.wf(oc.r) // outline_color.r u_data.wf(oc.g) // outline_color.g u_data.wf(oc.b) // outline_color.b u_data.wf(oc.a || 1) // outline_color.a - - cmd_buffer.push_fragment_uniform_data(0, stone(u_data)) - + + ctx.cmd_buffer.push_fragment_uniform_data(0, stone(u_data)) + } else if (is_sdf && _pipelines.text_sdf) { - pass.bind_pipeline(_pipelines.text_sdf) - + ctx.pass.bind_pipeline(_pipelines.text_sdf) + // Build uniforms for SDF // Struct: float outline_width, float sharpness, float2 _pad, float4 outline_color - var u_data = blob_mod(32) - - var outline_w = drawable.outline_width || 0 + u_data = blob_mod(32) + + outline_w = drawable.outline_width || 0 if (outline_w > 0) outline_w = outline_w / 100.0 - + u_data.wf(outline_w) // outline_width u_data.wf(font.sharpness || 1.0) // sharpness from font u_data.wf(0) // _pad.x u_data.wf(0) // _pad.y - - var oc = drawable.outline_color || {r:0, g:0, b:0, a:1} + + oc = drawable.outline_color || {r:0, g:0, b:0, a:1} u_data.wf(oc.r) u_data.wf(oc.g) u_data.wf(oc.b) u_data.wf(oc.a || 1) - - cmd_buffer.push_fragment_uniform_data(0, stone(u_data)) - + + ctx.cmd_buffer.push_fragment_uniform_data(0, stone(u_data)) + } else { - pass.bind_pipeline(_pipelines.sprite_alpha) + ctx.pass.bind_pipeline(_pipelines.sprite_alpha) } - pass.bind_vertex_buffers(0, [{buffer: vb, offset: 0}]) - pass.bind_index_buffer({buffer: ib, offset: 0}, 16) - + ctx.pass.bind_vertex_buffers(0, [{buffer: vb, offset: 0}]) + ctx.pass.bind_index_buffer({buffer: ib, offset: 0}, 16) + // Bind font texture - use linear filtering for SDF/MSDF var font_tex = _get_font_texture(font, mode) var sampler = (is_sdf || is_msdf) ? _sampler_linear : _sampler_nearest - - pass.bind_fragment_samplers(0, [{texture: font_tex, sampler: sampler}]) - cmd_buffer.push_vertex_uniform_data(0, proj) - pass.draw_indexed(num_indices, 1, 0, 0, 0) + + ctx.pass.bind_fragment_samplers(0, [{texture: font_tex, sampler: sampler}]) + ctx.cmd_buffer.push_vertex_uniform_data(0, proj) + ctx.pass.draw_indexed(num_indices, 1, 0, 0, 0) } function _get_font_cache(path, size, mode) { // mode can be 'bitmap', 'sdf', 'msdf', or boolean (legacy) - if (mode == true) mode = 'sdf' - else if (mode == false || !mode) mode = 'bitmap' - - var key = `${path}.${size}.${mode}` + var local_mode = mode + if (local_mode == true) local_mode = 'sdf' + else if (local_mode == false || !local_mode) local_mode = 'bitmap' + + var key = `${path}.${size}.${local_mode}` if (_font_cache[key]) return _font_cache[key] var fullpath = res.find_font(path) @@ -1923,24 +1951,23 @@ function _get_font_cache(path, size, mode) { if (!data) return null // Create staef font based on mode - try { - var font - if (mode == 'msdf') { - // MSDF: em_px=size, range_px=4, padding_px=6, sharpness=1.0 - font =staef.msdf_font(data, size, 4.0, 6, 1.0) - } else if (mode == 'sdf') { - // SDF: em_px=size, range_px=12, padding_px=14, sharpness=1.0 - font =staef.sdf_font(data, size, 12.0, 14, 1.0) - } else { - // Bitmap - font =staef.font(data, size, false) - } - _font_cache[key] = font - return font - } catch(e) { - log.console(`sdl_gpu: Failed to load font ${path}:${size}:${mode}: ${e.message}`) + var font = null + if (local_mode == 'msdf') { + // MSDF: em_px=size, range_px=4, padding_px=6, sharpness=1.0 + font =staef.msdf_font(data, size, 4.0, 6, 1.0) + } else if (local_mode == 'sdf') { + // SDF: em_px=size, range_px=12, padding_px=14, sharpness=1.0 + font =staef.sdf_font(data, size, 12.0, 14, 1.0) + } else { + // Bitmap + font =staef.font(data, size, false) + } + if (!font) { + log.console(`sdl_gpu: Failed to load font ${path}:${size}:${local_mode}`) return null } + _font_cache[key] = font + return font } @@ -1968,13 +1995,18 @@ function _do_blit(cmd_buffer, cmd, current_target, get_swapchain_tex) { if (!src || !src.texture) return if (!target) return + var swap_tex = null + var pass = null + var win_size = null + var geom = null + if (target == 'screen' || (!target.texture && target.width)) { // Cannot use SDL_BlitGPUTexture for screen/swapchain, must use render pass - var swap_tex = (target == 'screen') ? get_swapchain_tex() : target.texture + swap_tex = (target == 'screen') ? get_swapchain_tex() : target.texture if (!swap_tex && target == 'screen') swap_tex = get_swapchain_tex() if (!swap_tex) return - - var pass = cmd_buffer.render_pass({ + + pass = cmd_buffer.render_pass({ color_targets: [{ texture: swap_tex, load: "load", // Load existing content to blend layers properly @@ -1982,18 +2014,18 @@ function _do_blit(cmd_buffer, cmd, current_target, get_swapchain_tex) { }] }) - var win_size = sdl_gpu.get_window_size() - var geom = _build_fullscreen_quad(dst_rect, win_size.width, win_size.height) - - _draw_textured_quad(pass, geom, src.texture, _pipelines.blit, filter) + win_size = sdl_gpu.get_window_size() + geom = _build_fullscreen_quad(dst_rect, win_size.width, win_size.height) + + _draw_textured_quad(pass, geom, src.texture, {pipeline: _pipelines.blit, filter: filter}) pass.end() } else { // Use render pass with alpha blending instead of SDL blit (which overwrites) if (!target || !target.texture) return - - var geom = _build_fullscreen_quad(dst_rect, target.width, target.height) - - var pass = cmd_buffer.render_pass({ + + geom = _build_fullscreen_quad(dst_rect, target.width, target.height) + + pass = cmd_buffer.render_pass({ color_targets: [{ texture: target.texture, load: "load", // IMPORTANT: Load existing content so we blend on top @@ -2001,12 +2033,14 @@ function _do_blit(cmd_buffer, cmd, current_target, get_swapchain_tex) { }] }) - _draw_textured_quad(pass, geom, src.texture, _pipelines.blit, filter) + _draw_textured_quad(pass, geom, src.texture, {pipeline: _pipelines.blit, filter: filter}) pass.end() } } -function _draw_textured_quad(pass, geom, texture, pipeline, filter) { +function _draw_textured_quad(pass, geom, texture, opts) { + var pipeline = opts.pipeline + var filter = opts.filter var vb_size = length(geom.vertices) / 8 var ib_size = length(geom.indices) / 8 @@ -2122,25 +2156,19 @@ function _do_shader_pass(cmd_buffer, cmd, get_swapchain_tex) { // Select pipeline based on shader type var pipeline = null - switch (shader) { - case 'threshold': - pipeline = _pipelines.threshold - break - case 'blur': - pipeline = _pipelines.blur - break - case 'crt': - pipeline = _pipelines.crt - break - case 'accumulator': - pipeline = _pipelines.accumulator - break - case 'mask': - pipeline = _pipelines.mask - break - default: - log.console(`sdl_gpu: Unknown shader: ${shader}`) - return + if (shader == 'threshold') { + pipeline = _pipelines.threshold + } else if (shader == 'blur') { + pipeline = _pipelines.blur + } else if (shader == 'crt') { + pipeline = _pipelines.crt + } else if (shader == 'accumulator') { + pipeline = _pipelines.accumulator + } else if (shader == 'mask') { + pipeline = _pipelines.mask + } else { + log.console(`sdl_gpu: Unknown shader: ${shader}`) + return } if (!pipeline) { @@ -2177,10 +2205,11 @@ function _do_shader_pass(cmd_buffer, cmd, get_swapchain_tex) { var uniform_data = _build_shader_uniforms(shader, uniforms) // Start render pass to output target - var pass - + var pass = null + var swap_tex = null + if (output == 'screen') { - var swap_tex = get_swapchain_tex() + swap_tex = get_swapchain_tex() if (swap_tex) { pass = cmd_buffer.render_pass({ color_targets: [{ @@ -2234,49 +2263,46 @@ function _do_shader_pass(cmd_buffer, cmd, get_swapchain_tex) { function _build_shader_uniforms(shader, uniforms) { var data = blob_mod(64) // 16 floats max + var dir = null + var texel = null + var res = null - switch (shader) { - case 'threshold': - data.wf(uniforms.threshold || 0.8) - data.wf(uniforms.intensity || 1.0) - data.wf(0) // padding - data.wf(0) // padding - break - case 'blur': - var dir = uniforms.direction || {x: 1, y: 0} - var texel = uniforms.texel_size || {x: 0.001, y: 0.001} - data.wf(dir.x) - data.wf(dir.y) - data.wf(texel.x) - data.wf(texel.y) - break - case 'crt': - data.wf(uniforms.curvature || 0.1) - data.wf(uniforms.scanline_intensity || 0.3) - data.wf(uniforms.vignette || 0.2) - data.wf(0) // padding - var res = uniforms.resolution || {width: 1280, height: 720} - data.wf(res.width) - data.wf(res.height) - data.wf(0) // padding - data.wf(0) // padding - break - case 'accumulator': - data.wf(uniforms.decay != null ? uniforms.decay : 0.9) - data.wf(0) // padding - data.wf(0) // padding - data.wf(0) // padding - break - case 'mask': - // channel: 0=alpha, 1=luminance - // invert: 0=normal, 1=inverted - data.wf(uniforms.channel != null ? uniforms.channel : 0) - data.wf(uniforms.invert != null ? uniforms.invert : 0) - data.wf(0) // padding - data.wf(0) // padding - break - default: - return null + if (shader == 'threshold') { + data.wf(uniforms.threshold || 0.8) + data.wf(uniforms.intensity || 1.0) + data.wf(0) // padding + data.wf(0) // padding + } else if (shader == 'blur') { + dir = uniforms.direction || {x: 1, y: 0} + texel = uniforms.texel_size || {x: 0.001, y: 0.001} + data.wf(dir.x) + data.wf(dir.y) + data.wf(texel.x) + data.wf(texel.y) + } else if (shader == 'crt') { + data.wf(uniforms.curvature || 0.1) + data.wf(uniforms.scanline_intensity || 0.3) + data.wf(uniforms.vignette || 0.2) + data.wf(0) // padding + res = uniforms.resolution || {width: 1280, height: 720} + data.wf(res.width) + data.wf(res.height) + data.wf(0) // padding + data.wf(0) // padding + } else if (shader == 'accumulator') { + data.wf(uniforms.decay != null ? uniforms.decay : 0.9) + data.wf(0) // padding + data.wf(0) // padding + data.wf(0) // padding + } else if (shader == 'mask') { + // channel: 0=alpha, 1=luminance + // invert: 0=normal, 1=inverted + data.wf(uniforms.channel != null ? uniforms.channel : 0) + data.wf(uniforms.invert != null ? uniforms.invert : 0) + data.wf(0) // padding + data.wf(0) // padding + } else { + return null } return stone(data) diff --git a/sound.cm b/sound.cm index 1400eb5a..fb7c1d63 100644 --- a/sound.cm +++ b/sound.cm @@ -35,16 +35,16 @@ var player = soundwave.create({ // Load and cache PCM data from a file path audio.pcm = function pcm(file) { - file = res.find_sound(file) - if (!file) return null - + var path = res.find_sound(file) + if (!path) return null + // Check player's cache first - if (player.pcm_cache[file]) return player.pcm_cache[file] - - var buf = io.slurp(file) + if (player.pcm_cache[path]) return player.pcm_cache[path] + + var buf = io.slurp(path) if (!buf) return null - - return player.decode(buf, file) + + return player.decode(buf, path) } // Play a sound file, returns voice object @@ -73,11 +73,12 @@ feeder.resume_device() // Audio pump - called periodically to fill the audio buffer function pump() { + var mixed = null while (feeder.queued() < CHUNK_BYTES * 3) { - var mixed = player.pull(FRAMES_PER_CHUNK) + mixed = player.pull(FRAMES_PER_CHUNK) feeder.put(mixed) } - + $delay(pump, 1/240) } diff --git a/sprite.c b/sprite.c index 6a39150a..dd0741b0 100644 --- a/sprite.c +++ b/sprite.c @@ -46,13 +46,7 @@ void sprite_apply(sprite *sp) // Sprite class definitions static JSClassID js_sprite_id; -static void js_sprite_mark(JSRuntime *rt, JSValueConst val, JS_MarkFunc *mark_func) { - sprite *sp = JS_GetOpaque(val, js_sprite_id); - if (!sp) return; - JS_MarkValue(rt, sp->image, mark_func); -} -// Class definition for sprite with mark function for GC QJSCLASSMARK(sprite,) // SPRITE ACTION FUNCTIONS @@ -135,7 +129,7 @@ static JSValue js_sprite_constructor(JSContext *js, JSValueConst new_target, int JS_GETATOM(js, sp->layer, argv[0], layer, number) JS_GETATOM(js, sp->color, argv[0], color, color) - JSValue image = JS_GetProperty(js, argv[0], JS_NewAtom(js, "image")); + JSValue image = JS_GetPropertyStr(js, argv[0], "image"); if (!JS_IsNull(image)) { sp->image = image; // Transfer ownership, no need to dup } diff --git a/tests/animation.ce b/tests/animation.ce index 87deaad3..f9c9d992 100644 --- a/tests/animation.ce +++ b/tests/animation.ce @@ -2,20 +2,22 @@ var Anim = (() => { def DEFAULT_MIN = 1 / 60; /* 16 ms – one frame */ - function play(source, loop=true){ + function play(source, loop){ + var local_loop = !is_null(loop) ? loop : (!is_null(source.loop) ? source.loop : true); return { src : source, idx : 0, timer : 0, - loop : loop ?? source.loop ?? true + loop : local_loop }; } function update(a, dt){ a.timer += dt; - def frames = a.src.frames; + var frames = a.src.frames; + var time = null; while(true){ - def time = max(frames[a.idx].time || 0, Anim.minDelay); + time = max(frames[a.idx].time || 0, Anim.minDelay); if(a.timer < time) break; /* still on current frame */ a.timer -= time; diff --git a/tests/bunnymark.ce b/tests/bunnymark.ce index 9cf7e30c..1ccbeccd 100644 --- a/tests/bunnymark.ce +++ b/tests/bunnymark.ce @@ -52,10 +52,14 @@ function hsl_to_rgb(h, s, l) { var bunny_count = 20 -for (var i = 0; i < bunny_count; i++) { - var pct = i/bunny_count - var hue = 270 * i / bunny_count - var sp = sprite.create(bunny, { +var i = 0; +var pct = 0; +var hue = 0; +var sp = null; +for (i = 0; i < bunny_count; i++) { + pct = i/bunny_count + hue = 270 * i / bunny_count + sp = sprite.create(bunny, { pos: [random.random()*dim.x*pct, random.random()*dim.y*pct], center, color: hsl_to_rgb(hue, 0.5, 0.5) diff --git a/tests/camera_colorspace.ce b/tests/camera_colorspace.ce index 5e481059..3a23a28d 100644 --- a/tests/camera_colorspace.ce +++ b/tests/camera_colorspace.ce @@ -18,8 +18,10 @@ log.console("\nLooking for different colorspaces in supported formats..."); // Group formats by colorspace var colorspaces = {}; -for (var i = 0; i < length(formats); i++) { - var fmt = formats[i]; +var i = 0; +var fmt = null; +for (i = 0; i < length(formats); i++) { + fmt = formats[i]; if (!colorspaces[fmt.colorspace]) { colorspaces[fmt.colorspace] = []; } @@ -53,8 +55,9 @@ arrfor(array(colorspaces), function(cs) { }; var cam = camera.open(cam_id, custom_format); + var actual = null; if (cam) { - var actual = cam.get_format(); + actual = cam.get_format(); log.console(" Opened successfully!"); log.console(" Actual colorspace: " + actual.colorspace); diff --git a/tests/camera_colorspace_convert.ce b/tests/camera_colorspace_convert.ce index 0e7515d6..9132d248 100644 --- a/tests/camera_colorspace_convert.ce +++ b/tests/camera_colorspace_convert.ce @@ -40,67 +40,76 @@ $receiver(e => { }); // Wait for approval then capture +var srgb_surf = null; +var linear_surf = null; +var jpeg_surf = null; +var hd_surf = null; + function capture_test() { if (!approved) { $delay(capture_test, 0.1); return; } - + log.console("\nCapturing frame..."); var surf = cam.capture(); - + if (!surf) { log.console("No frame captured yet, retrying..."); $delay(capture_test, 0.1); return; } - + log.console("\nCaptured surface:"); log.console(" Size:", surf.width + "x" + surf.height); log.console(" Format:", surf.format); - + // Test various colorspace conversions log.console("\nTesting colorspace conversions:"); - + // Convert to sRGB if not already + var convert_srgb = function() { + srgb_surf = surf.convert(surf.format, "srgb"); + log.console(" Converted to sRGB colorspace"); + } disruption { + log.console(" sRGB conversion failed"); + } if (format.colorspace != "srgb") { - try { - var srgb_surf = surf.convert(surf.format, "srgb"); - log.console(" Converted to sRGB colorspace"); - } catch(e) { - log.console(" sRGB conversion failed:", e.message); - } + convert_srgb(); } - + // Convert to linear sRGB for processing - try { - var linear_surf = surf.convert("rgba8888", "srgb_linear"); + var convert_linear = function() { + linear_surf = surf.convert("rgba8888", "srgb_linear"); log.console(" Converted to linear sRGB (RGBA8888) for processing"); - } catch(e) { - log.console(" Linear sRGB conversion failed:", e.message); + } disruption { + log.console(" Linear sRGB conversion failed"); } - + convert_linear(); + // Convert to JPEG colorspace (common for compression) - try { - var jpeg_surf = surf.convert("rgb888", "jpeg"); + var convert_jpeg = function() { + jpeg_surf = surf.convert("rgb888", "jpeg"); log.console(" Converted to JPEG colorspace (RGB888) for compression"); - } catch(e) { - log.console(" JPEG colorspace conversion failed:", e.message); + } disruption { + log.console(" JPEG colorspace conversion failed"); } - + convert_jpeg(); + // If YUV format, try BT.709 (HD video standard) - if (search(surf.format, "yuv") != null || search(surf.format, "yuy") != null) { - try { - var hd_surf = surf.convert(surf.format, "bt709_limited"); - log.console(" Converted to BT.709 limited (HD video standard)"); - } catch(e) { - log.console(" BT.709 conversion failed:", e.message); - } + var convert_hd = function() { + hd_surf = surf.convert(surf.format, "bt709_limited"); + log.console(" Converted to BT.709 limited (HD video standard)"); + } disruption { + log.console(" BT.709 conversion failed"); } - + if (search(surf.format, "yuv") != null || search(surf.format, "yuy") != null) { + convert_hd(); + } + log.console("\nTest complete!"); $stop(); } // Start capture test after a short delay -$delay(capture_test, 0.5); \ No newline at end of file +$delay(capture_test, 0.5); diff --git a/tests/camera_info.ce b/tests/camera_info.ce index b685faa2..b7c53b2b 100644 --- a/tests/camera_info.ce +++ b/tests/camera_info.ce @@ -10,23 +10,32 @@ var cameras = camera.list(); log.console("Found", length(cameras), "cameras"); // Get info about each camera -for (var i = 0; i < length(cameras); i++) { - var cam_id = cameras[i]; +var i = 0; +var j = 0; +var cam_id = null; +var formats = null; +var fmt = null; +var preferred_format = null; +var cam = null; +var actual_format = null; + +for (i = 0; i < length(cameras); i++) { + cam_id = cameras[i]; log.console("\nCamera", i + 1, "ID:", cam_id); log.console(" Name:", camera.name(cam_id)); log.console(" Position:", camera.position(cam_id)); - + // Get supported formats - var formats = camera.supported_formats(cam_id); + formats = camera.supported_formats(cam_id); log.console(" Supported formats:", length(formats)); - + // Show first few formats - for (var j = 0; j < length(formats); j++) { - var fmt = formats[j]; + for (j = 0; j < length(formats); j++) { + fmt = formats[j]; log.console(" Format", j + 1 + ":"); log.console(" Pixel format:", fmt.format); log.console(" Resolution:", fmt.width + "x" + fmt.height); - log.console(" FPS:", fmt.framerate_numerator + "/" + fmt.framerate_denominator, + log.console(" FPS:", fmt.framerate_numerator + "/" + fmt.framerate_denominator, "(" + (fmt.framerate_numerator / fmt.framerate_denominator) + ")"); log.console(" Colorspace:", fmt.colorspace); } @@ -35,19 +44,19 @@ for (var i = 0; i < length(cameras); i++) { // Open the first camera with a specific format if available if (length(cameras) > 0) { log.console("\nOpening first camera..."); - var cam_id = cameras[0]; - var formats = camera.supported_formats(cam_id); - + cam_id = cameras[0]; + formats = camera.supported_formats(cam_id); + // Try to find a 640x480 format - var preferred_format = null; - for (var i = 0; i < length(formats); i++) { + preferred_format = null; + for (i = 0; i < length(formats); i++) { if (formats[i].width == 640 && formats[i].height == 480) { preferred_format = formats[i]; break; } } - - var cam; + + cam = null; if (preferred_format) { log.console("Opening with 640x480 format..."); cam = camera.open(cam_id, preferred_format); @@ -55,21 +64,21 @@ if (length(cameras) > 0) { log.console("Opening with default format..."); cam = camera.open(cam_id); } - + if (cam) { log.console("Camera opened successfully!"); log.console("Driver being used:", cam.get_driver()); - + // Get the actual format being used - var actual_format = cam.get_format(); + actual_format = cam.get_format(); log.console("Actual format being used:"); log.console(" Pixel format:", actual_format.format); log.console(" Resolution:", actual_format.width + "x" + actual_format.height); log.console(" FPS:", actual_format.framerate_numerator + "/" + actual_format.framerate_denominator, "(" + (actual_format.framerate_numerator / actual_format.framerate_denominator) + ")"); log.console(" Colorspace:", actual_format.colorspace); - + // Clean up - camera will be closed when object is freed cam = null; } -} \ No newline at end of file +} diff --git a/tests/draw2d.ce b/tests/draw2d.ce index c5296861..66946749 100644 --- a/tests/draw2d.ce +++ b/tests/draw2d.ce @@ -1,6 +1,6 @@ // Test draw2d module without moth framework -var draw2d -var graphics +var draw2d = null +var graphics = null var os = use('os'); var input = use('input') var math = use('math/radians') @@ -170,11 +170,16 @@ function start_drawing() { // Draw some points in a pattern var point_count = 20; - for (var i = 0; i < point_count; i++) { - var angle = (i / point_count) * pi * 2; - var r = 30 + math.sine(t * 4 + i * 0.5) * 10; - var px = 650 + math.cosine(angle) * r; - var py = 300 + math.sine(angle) * r; + var i = 0; + var angle = 0; + var r = 0; + var px = 0; + var py = 0; + for (i = 0; i < point_count; i++) { + angle = (i / point_count) * pi * 2; + r = 30 + math.sine(t * 4 + i * 0.5) * 10; + px = 650 + math.cosine(angle) * r; + py = 300 + math.sine(angle) * r; draw2d.point( [px, py], diff --git a/tests/surface_colorspace.ce b/tests/surface_colorspace.ce index 7e2ff3fd..72cbf9f6 100644 --- a/tests/surface_colorspace.ce +++ b/tests/surface_colorspace.ce @@ -31,13 +31,17 @@ var colorspaces = ["srgb", "srgb_linear", "jpeg", "bt601_limited", "bt709_limite var test_format = "rgba8888"; log.console("\nTest 3: Converting to", test_format, "with different colorspaces:"); -for (var i = 0; i < length(colorspaces); i++) { - try { - var conv = surf.convert(test_format, colorspaces[i]); +var i = 0; +var conv = null; +var try_convert = null; +for (i = 0; i < length(colorspaces); i++) { + try_convert = function() { + conv = surf.convert(test_format, colorspaces[i]); log.console(" " + colorspaces[i] + ": Success"); - } catch(e) { - log.console(" " + colorspaces[i] + ": Failed -", e.message); + } disruption { + log.console(" " + colorspaces[i] + ": Failed"); } + try_convert(); } // Test 4: YUV formats with appropriate colorspaces @@ -49,15 +53,18 @@ var yuv_tests = [ {format: "yvyu", colorspace: "bt601_full"} ]; -for (var i = 0; i < length(yuv_tests); i++) { - var test = yuv_tests[i]; - try { - var conv = surf.convert(test.format, test.colorspace); +var test = null; +var try_yuv = null; +for (i = 0; i < length(yuv_tests); i++) { + test = yuv_tests[i]; + try_yuv = function() { + conv = surf.convert(test.format, test.colorspace); log.console(" " + test.format + " with " + test.colorspace + ": Success"); - } catch(e) { - log.console(" " + test.format + " with " + test.colorspace + ": Failed -", e.message); + } disruption { + log.console(" " + test.format + " with " + test.colorspace + ": Failed"); } + try_yuv(); } log.console("\nColorspace conversion test complete!"); -$stop(); \ No newline at end of file +$stop(); diff --git a/tests/webcam.ce b/tests/webcam.ce index d24ddcc3..aa770633 100644 --- a/tests/webcam.ce +++ b/tests/webcam.ce @@ -1,6 +1,6 @@ // Test webcam display -var draw2d -var graphics +var draw2d = null +var graphics = null var os = use('os'); var input = use('input') var json = use('json') @@ -88,7 +88,8 @@ send(video_actor, { // Look for a 640x480 format with preferred colorspace var preferred_format = null; - for (var i = 0; i < length(formats); i++) { + var i = 0; + for (i = 0; i < length(formats); i++) { if (formats[i].width == 640 && formats[i].height == 480) { preferred_format = formats[i]; // Prefer JPEG or sRGB colorspace if available diff --git a/transform.c b/transform.c index fb61e241..93d1c16a 100644 --- a/transform.c +++ b/transform.c @@ -182,16 +182,6 @@ transform mat2transform(HMM_Mat4 m) // Transform class definitions JSClassID js_transform_id; -static void js_transform_mark(JSRuntime *rt, JSValueConst val, JS_MarkFunc *mark_func) { - transform *t = JS_GetOpaque(val, js_transform_id); - if (!t) return; - // Mark the JSValue references stored in your struct - JS_MarkValue(rt, t->change_hook, mark_func); - JS_MarkValue(rt, t->jsparent, mark_func); - // Mark the array elements - for (int i = 0; i < arrlen(t->jschildren); i++) - JS_MarkValue(rt, t->jschildren[i], mark_func); -} QJSCLASSMARK_EXTERN(transform,) @@ -241,7 +231,7 @@ static JSValue js_transform_set_parent(JSContext *js, JSValueConst self, JSValue } for (int i = 0; i < arrlen(cur_parent->jschildren); i++) { - if (JS_SameValue(js,cur_parent->jschildren[i],self)) { + if (JS_StrictEq(js,cur_parent->jschildren[i],self)) { JS_FreeValue(js,cur_parent->jschildren[i]); arrdelswap(cur_parent->jschildren,i); break; @@ -341,7 +331,7 @@ JSC_CCALL(transform_array, HMM_Mat4 m= transform2mat(t); ret = JS_NewArray(js); for (int i = 0; i < 16; i++) - JS_SetPropertyUint32(js,ret,i, number2js(js,m.em[i])); + JS_SetPropertyNumber(js,ret,i, number2js(js,m.em[i])); ) JSC_CCALL(transform_torect, @@ -353,7 +343,7 @@ JSC_CCALL(transform_children, transform *t = js2transform(js,self); ret = JS_NewArray(js); for (int i = 0; i < arrlen(t->jschildren); i++) - JS_SetPropertyUint32(js,ret,i,JS_DupValue(js,t->jschildren[i])); + JS_SetPropertyNumber(js,ret,i,JS_DupValue(js,t->jschildren[i])); ) static const JSCFunctionListEntry js_transform_funcs[] = { diff --git a/tween.cm b/tween.cm index 2fed6d5d..ba0eb696 100644 --- a/tween.cm +++ b/tween.cm @@ -14,11 +14,12 @@ function make_engine(default_clock) { this.tweens = filter(this.tweens, t => t != tween) }, update(current_time) { - if (current_time == null) { - current_time = this.default_clock ? this.default_clock() : time.number() + var ct = current_time + if (ct == null) { + ct = this.default_clock ? this.default_clock() : time.number() } arrfor(this.tweens, function(tween) { - tween._update(current_time) + tween._update(ct) }) }, clear() { @@ -47,7 +48,7 @@ var TweenProto = { if (is_object(value)) { arrfor(array(value), subkey => { var flatKey = key + '.' + subkey - this.startVals[flatKey] = this.obj[key] ? this.obj[key][subkey] : undefined + this.startVals[flatKey] = this.obj[key] ? this.obj[key][subkey] : null this.endVals[flatKey] = value[subkey] }) } else { @@ -87,7 +88,7 @@ var TweenProto = { _update: function(now) { this.seek(now) - this.onUpdateCallback?.() + if (this.onUpdateCallback) this.onUpdateCallback() }, seek: function(global_time) { @@ -99,11 +100,14 @@ var TweenProto = { var start = this.startVals[key] var end = this.endVals[key] var value = start + (end - start) * eased + var parts = null + var objKey = null + var subKey = null if (search(key, '.') != null) { - var parts = array(key, '.') - var objKey = parts[0] - var subKey = parts[1] + parts = array(key, '.') + objKey = parts[0] + subKey = parts[1] if (!this.obj[objKey]) { this.obj[objKey] = {}