fix syntax

This commit is contained in:
2026-02-17 09:15:15 -06:00
parent f310c18b84
commit 4e1b63fd0e
52 changed files with 2169 additions and 1754 deletions

100
action.cm
View File

@@ -25,55 +25,60 @@ action.get_icon_for_action = function(action)
{ {
var bindings = this.get_bindings_for_device(action) var bindings = this.get_bindings_for_device(action)
if (!length(bindings)) return null if (!length(bindings)) return null
var primary_binding = bindings[0] 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 (this.current_device == 'keyboard') {
if (starts_with(primary_binding, 'mouse_button_')) { 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' return 'ui/mouse/mouse_' + button + '.png'
} else { } else {
// Handle special keyboard keys // Handle special keyboard keys
var key_mapping = { key = key_mapping[primary_binding] || primary_binding
'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
return 'ui/keyboard/keyboard_' + key + '.png' return 'ui/keyboard/keyboard_' + key + '.png'
} }
} }
if (this.current_device == 'gamepad' && this.current_gamepad_type) { 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 // 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_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_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_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_dpup': controller_prefix + '_dpad_up',
'gamepad_dpdown': controller_prefix + '_dpad_down', 'gamepad_dpdown': controller_prefix + '_dpad_down',
'gamepad_dpleft': controller_prefix + '_dpad_left', 'gamepad_dpleft': controller_prefix + '_dpad_left',
'gamepad_dpright': controller_prefix + '_dpad_right', 'gamepad_dpright': controller_prefix + '_dpad_right',
'gamepad_l1': controller_prefix + '_trigger_l1', '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_l2': controller_prefix + '_trigger_l2',
'gamepad_r2': controller_prefix + '_trigger_r2', 'gamepad_r2': controller_prefix + '_trigger_r2',
'gamepad_start': this.get_start_button_icon() 'gamepad_start': this.get_start_button_icon()
} }
var icon_name = gamepad_mapping[primary_binding] icon_name = gamepad_mapping[primary_binding]
if (icon_name) { if (icon_name) {
return 'ui/' + controller_prefix + '/' + icon_name + '.png' return 'ui/' + controller_prefix + '/' + icon_name + '.png'
} }
} }
return null return null
} }
@@ -181,23 +186,26 @@ var SWIPE_MAX_TIME = 500 // ms
action.on_input = function(action_id, evt) action.on_input = function(action_id, evt)
{ {
// 1) Detect & store which device user is on (only from raw input, ignore passive inputs) // 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) { 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 // 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 // This helps distinguish real keyboard/mouse events from mapped actions
var is_real_kb_mouse = (new_device == 'keyboard' && is_real_kb_mouse = (new_device == 'keyboard' &&
(evt.ctrl != null || evt.shift != null || evt.alt != null || (evt.ctrl != null || evt.shift != null || evt.alt != null ||
starts_with(action_id, 'mouse_button'))) starts_with(action_id, 'mouse_button')))
if (new_device == 'keyboard' && !is_real_kb_mouse) { if (new_device == 'keyboard' && !is_real_kb_mouse) {
// This might be a mapped action, not a real keyboard/mouse event // This might be a mapped action, not a real keyboard/mouse event
return return
} }
if (new_device != this.current_device) { if (new_device != this.current_device) {
if (new_device == 'gamepad') { 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 + ")") log.console("Input switched to 'gamepad' (event: " + action_id + ", type: " + gamepad_type + ")")
this.current_gamepad_type = gamepad_type this.current_gamepad_type = gamepad_type
} else if (this.current_device == 'gamepad') { } 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) // Send all matched actions (only if we found mappings - this means it's a raw input)
var i = 0
if (length(matched_actions) > 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]) // 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 // Clear existing bindings for the current device from the target action
var target_bindings = this.action_map[action_name] 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) 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)) 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() { action.save_bindings = function() {
try { var self = this
io.slurpwrite('keybindings.json', json.encode(this.action_map)) var _save = function() {
} catch(e) { io.slurpwrite('keybindings.json', json.encode(self.action_map))
log.console("Failed to save key bindings:", e) } disruption {
log.console("Failed to save key bindings")
} }
_save()
} }
action.load_bindings = function() { action.load_bindings = function() {
try { var self = this
var _load = function() {
var data = null
var bindings = null
if (io.exists('keybindings.json')) { if (io.exists('keybindings.json')) {
var data = io.slurp('keybindings.json') data = io.slurp('keybindings.json')
var bindings = object(json.decode(data), this.action_map) bindings = object(json.decode(data), self.action_map)
} }
} catch(e) { } disruption {
log.console("Failed to load key bindings:", e) log.console("Failed to load key bindings")
} }
_load()
} }
action.reset_to_defaults = function() { action.reset_to_defaults = function() {

137
clay.cm
View File

@@ -38,13 +38,13 @@ var base_config = {
} }
function normalize_color(c, fallback) { function normalize_color(c, fallback) {
fallback = fallback || {r:1, g:1, b:1, a:1} var fb = fallback || {r:1, g:1, b:1, a:1}
if (!c) return {r:fallback.r, g:fallback.g, b:fallback.b, a:fallback.a} if (!c) return {r:fb.r, g:fb.g, b:fb.b, a:fb.a}
return { return {
r: c.r != null ? c.r : fallback.r, r: c.r != null ? c.r : fb.r,
g: c.g != null ? c.g : fallback.g, g: c.g != null ? c.g : fb.g,
b: c.b != null ? c.b : fallback.b, b: c.b != null ? c.b : fb.b,
a: c.a != null ? c.a : fallback.a a: c.a != null ? c.a : fb.a
} }
} }
@@ -59,8 +59,8 @@ function normalize_spacing(s) {
} }
// Tree building state // Tree building state
var root_item var root_item = null
var tree_root var tree_root = null
var config_stack = [] var config_stack = []
// Rewriting state management for cleaner recursion // Rewriting state management for cleaner recursion
@@ -89,11 +89,11 @@ clay.layout = function(fn, size) {
return build_drawables(root_node, size.height) return build_drawables(root_node, size.height)
} }
function build_drawables(node, root_height, parent_abs_x, parent_abs_y, parent_scissor, parent_layer) { function build_drawables(node, root_height, parent, parent_scissor) {
parent_abs_x = parent_abs_x || 0 var p_abs_x = (parent && parent.x) || 0
parent_abs_y = parent_abs_y || 0 var p_abs_y = (parent && parent.y) || 0
parent_layer = parent_layer || 0 // UI usually on top, but let's start at 0 var p_layer = (parent && parent.layer) || 0
var rect = lay_ctx.get_rect(node.id) var rect = lay_ctx.get_rect(node.id)
// Calculate absolute world Y for this node (bottom-up layout to top-down render) // 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 // Scissor
var current_scissor = parent_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) { if (node.config.clipped) {
var sx = vis_x
var sy = vis_y
var sw = rect.width
var sh = rect.height
// Intersect with parent // Intersect with parent
if (parent_scissor) { if (parent_scissor) {
sx = max(sx, parent_scissor.x) sx = max(sx, parent_scissor.x)
sy = max(sy, parent_scissor.y) sy = max(sy, parent_scissor.y)
var right = min(vis_x + sw, parent_scissor.x + parent_scissor.width) clip_right = min(vis_x + sw, parent_scissor.x + parent_scissor.width)
var bottom = min(vis_y + sh, parent_scissor.y + parent_scissor.height) clip_bottom = min(vis_y + sh, parent_scissor.y + parent_scissor.height)
sw = max(0, right - sx) sw = max(0, clip_right - sx)
sh = max(0, bottom - sy) sh = max(0, clip_bottom - sy)
} }
current_scissor = {x: sx, y: sy, width: sw, height: sh} 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, height: rect.height,
slice: node.config.slice, slice: node.config.slice,
color: node.config.background_color || {r:1, g:1, b:1, a:1}, 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 scissor: current_scissor
}) })
} else { } else {
@@ -149,7 +150,7 @@ function build_drawables(node, root_height, parent_abs_x, parent_abs_y, parent_s
width: rect.width, width: rect.width,
height: rect.height, height: rect.height,
color: node.config.background_color || {r:1, g:1, b:1, a:1}, 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 scissor: current_scissor
}) })
} }
@@ -160,7 +161,7 @@ function build_drawables(node, root_height, parent_abs_x, parent_abs_y, parent_s
width: rect.width, width: rect.width,
height: rect.height, height: rect.height,
color: node.config.background_color, color: node.config.background_color,
layer: parent_layer - 0.1, layer: p_layer - 0.1,
scissor: current_scissor scissor: current_scissor
}) })
} }
@@ -174,7 +175,7 @@ function build_drawables(node, root_height, parent_abs_x, parent_abs_y, parent_s
width: rect.width, width: rect.width,
height: rect.height, height: rect.height,
color: node.config.color, color: node.config.color,
layer: parent_layer, layer: p_layer,
scissor: current_scissor 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. // `abs_y = root_height - (rect.y + rect.height)` -> Top edge of element.
// Text usually wants baseline. // Text usually wants baseline.
// If we put it at `vis_y + rect.height`, that's bottom of element. // If we put it at `vis_y + rect.height`, that's bottom of element.
layer: parent_layer, layer: p_layer,
scissor: current_scissor scissor: current_scissor
}) })
} }
// Children // Children
arrfor(node.children, function(child) { 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 return drawables
@@ -235,8 +236,8 @@ function push_node(configs, contain_mode) {
lay_ctx.set_contain(item, config.contain) lay_ctx.set_contain(item, config.contain)
lay_ctx.set_behave(item, config.behave) 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]} if (is_array(s)) s = {width: s[0], height: s[1]}
lay_ctx.set_size(item, s) lay_ctx.set_size(item, s)
} }
@@ -262,44 +263,44 @@ function pop_node() {
// Generic container // Generic container
clay.container = function(configs, fn) { clay.container = function(configs, fn) {
if (is_function(configs)) { fn = configs; configs = {} } var _fn = is_function(configs) ? configs : fn
if (!is_array(configs)) configs = [configs] var _configs = is_function(configs) ? [{}] : (is_array(configs) ? configs : [configs])
push_node(configs, null) push_node(_configs, null)
if (fn) fn() if (_fn) _fn()
pop_node() pop_node()
} }
// Stacks // Stacks
clay.vstack = function(configs, fn) { clay.vstack = function(configs, fn) {
if (is_function(configs)) { fn = configs; configs = {} } var _fn = is_function(configs) ? configs : fn
if (!is_array(configs)) configs = [configs] var _configs = is_function(configs) ? [{}] : (is_array(configs) ? configs : [configs])
var c = layout.contain.column var c = layout.contain.column
push_node(configs, c) push_node(_configs, c)
if (fn) fn() if (_fn) _fn()
pop_node() pop_node()
} }
clay.hstack = function(configs, fn) { clay.hstack = function(configs, fn) {
if (is_function(configs)) { fn = configs; configs = {} } var _fn = is_function(configs) ? configs : fn
if (!is_array(configs)) configs = [configs] var _configs = is_function(configs) ? [{}] : (is_array(configs) ? configs : [configs])
var c = layout.contain.row var c = layout.contain.row
push_node(configs, c) push_node(_configs, c)
if (fn) fn() if (_fn) _fn()
pop_node() pop_node()
} }
clay.zstack = function(configs, fn) { clay.zstack = function(configs, fn) {
if (is_function(configs)) { fn = configs; configs = {} } var _fn = is_function(configs) ? configs : fn
if (!is_array(configs)) configs = [configs] var _configs = is_function(configs) ? [{}] : (is_array(configs) ? configs : [configs])
var c = layout.contain.layout var c = layout.contain.layout
push_node(configs, c) push_node(_configs, c)
if (fn) fn() if (_fn) _fn()
pop_node() pop_node()
} }
@@ -308,12 +309,12 @@ clay.image = function(path, configs) {
var img = graphics.texture(path) var img = graphics.texture(path)
var c = [{image: path}] var c = [{image: path}]
var final_config = process_configs(configs) 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} c.size = {width: img.width, height: img.height}
if (!is_array(configs)) configs = [configs] var _configs = is_array(configs) ? configs : [configs]
push_node(array(c, configs), null) push_node(array(c, _configs), null)
pop_node() pop_node()
} }
@@ -321,18 +322,18 @@ clay.text = function(str, configs) {
var c = [{text: str}] var c = [{text: str}]
var final_config = process_configs(configs) var final_config = process_configs(configs)
if (!final_config.size && !final_config.behave) { 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] var _configs = is_array(configs) ? configs : [configs]
push_node(array(c, configs), null) push_node(array(c, _configs), null)
pop_node() pop_node()
} }
clay.rectangle = function(configs) { clay.rectangle = function(configs) {
if (!is_array(configs)) configs = [configs] var _configs = is_array(configs) ? configs : [configs]
push_node(configs, null) push_node(_configs, null)
pop_node() pop_node()
} }
@@ -341,10 +342,10 @@ clay.button = function(str, action, configs) {
padding: 10, padding: 10,
background_color: {r:0.3, g:0.3, b:0.4, a:1} background_color: {r:0.3, g:0.3, b:0.4, a:1}
}] }]
if (!is_array(configs)) configs = [configs] var _configs = is_array(configs) ? configs : [configs]
clay.zstack(array(btn_config, configs), function() { clay.zstack(array(btn_config, _configs), function() {
clay.text(str, {color: {r:1,g:1,b:1,a:1}}) clay.text(str, {color: {r:1,g:1,b:1,a:1}})
}) })
} }

View File

@@ -35,12 +35,16 @@ function find_path(node, path, pos) {
if (!rect_contains(node, pos)) return null if (!rect_contains(node, pos)) return null
var next_path = array(path, node) var next_path = array(path, node)
var i = 0
var child = null
var child_path = null
if (node[clay.CHILDREN] && !should_skip_children(node)) { if (node[clay.CHILDREN] && !should_skip_children(node)) {
// Children drawn later should be tested first; reverse if your render order differs // Children drawn later should be tested first; reverse if your render order differs
for (var i = length(node[clay.CHILDREN]) - 1; i >= 0; i--) { i = length(node[clay.CHILDREN]) - 1
var child = node[clay.CHILDREN][i] for (; i >= 0; i--) {
var child_path = find_path(child, next_path, pos) child = node[clay.CHILDREN][i]
child_path = find_path(child, next_path, pos)
if (child_path) return child_path if (child_path) return child_path
} }
} }
@@ -65,7 +69,8 @@ clay_input.bubble = function bubble(deepest, prop) {
return null 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 deepest = clay_input.deepest(tree_root, mousepos)
var action_target = clay_input.bubble(deepest, 'action') var action_target = clay_input.bubble(deepest, 'action')
if (action_target && action_target.config.action) action_target.config.action() if (action_target && action_target.config.action) action_target.config.action()

View File

@@ -181,24 +181,30 @@ ColorMap.Viridis = ColorMap.makemap({
Color.normalize(ColorMap); Color.normalize(ColorMap);
ColorMap.sample = function(t, map = this) { ColorMap.sample = function(t, map) {
if (t < 0) return map[0] var local_map = map || this
if (t > 1) return map[1] 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 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++) { for (i = 0; i < length(keys); i++) {
var key = keys[i] key = keys[i]
if (t < key) { if (t < key) {
var b = map[key] b = local_map[key]
var a = map[lastkey] a = local_map[lastkey]
var tt = (t - lastkey) / (key - lastkey) tt = (t - lastkey) / (key - lastkey)
return a.lerp(b, tt) return a.lerp(b, tt)
} }
lastkey = key lastkey = key
} }
return map[1] return local_map[1]
} }
ColorMap.doc = { ColorMap.doc = {

View File

@@ -27,9 +27,12 @@ compositor.compile = function(config) {
// Process each plane (supports both 'planes' and legacy 'layers' key) // Process each plane (supports both 'planes' and legacy 'layers' key)
var planes = config.planes || config.layers || [] var planes = config.planes || config.layers || []
for (var i = 0; i < length(planes); i++) { var i = 0
var plane = planes[i] var plane = null
var type = plane.type || 'film2d' var type = null
for (i = 0; i < length(planes); i++) {
plane = planes[i]
type = plane.type || 'film2d'
if (type == 'imgui') { if (type == 'imgui') {
compile_imgui_layer(plane, ctx) compile_imgui_layer(plane, ctx)
} else { } else {
@@ -58,7 +61,8 @@ function compile_plane(plane_config, ctx, group_effects) {
var mask_groups = {} var mask_groups = {}
arrfor(array(group_effects), gname => { arrfor(array(group_effects), gname => {
var effects = group_effects[gname].effects || [] 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) if (effects[e].type == 'mask' && effects[e].mask_group)
mask_groups[effects[e].mask_group] = true 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}) var all_sprites = film2d.query({plane: plane_name})
// Add manual drawables // Add manual drawables
var di = 0
if (plane_config.drawables) { if (plane_config.drawables) {
for (var i = 0; i < length(plane_config.drawables); i++) for (di = 0; di < length(plane_config.drawables); di++)
push(all_sprites, plane_config.drawables[i]) push(all_sprites, plane_config.drawables[di])
} }
// Find which sprites belong to groups with effects // Find which sprites belong to groups with effects
var effect_groups = {} // group_name -> {sprites: [], effects: []} var effect_groups = {} // group_name -> {sprites: [], effects: []}
var base_sprites = [] var base_sprites = []
for (var i = 0; i < length(all_sprites); i++) { var si = 0
var s = all_sprites[i] var s = null
var sprite_groups = s.groups || [] var sprite_groups = null
var assigned = false var assigned = false
var is_mask_only = length(sprite_groups) > 0 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 // First pass: check if sprite has any non-mask group
for (var g = 0; g < length(sprite_groups); g++) { for (g = 0; g < length(sprite_groups); g++) {
var gname = sprite_groups[g] gname = sprite_groups[g]
if (!mask_groups[gname]) { if (!mask_groups[gname]) {
is_mask_only = false is_mask_only = false
break break
} }
} }
// Second pass: assign to effect groups // Second pass: assign to effect groups
for (var g = 0; g < length(sprite_groups); g++) { for (g = 0; g < length(sprite_groups); g++) {
var gname = sprite_groups[g] gname = sprite_groups[g]
if (group_effects[gname]) { if (group_effects[gname]) {
if (!effect_groups[gname]) if (!effect_groups[gname])
effect_groups[gname] = {sprites: [], effects: group_effects[gname].effects} 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 break // Only assign to first matching effect group
} }
} }
// Add to base sprites if not assigned to effect group and not mask-only // Add to base sprites if not assigned to effect group and not mask-only
if (!assigned && !is_mask_only) push(base_sprites, s) if (!assigned && !is_mask_only) push(base_sprites, s)
} }
@@ -136,9 +148,11 @@ function compile_plane(plane_config, ctx, group_effects) {
// Apply effects // Apply effects
var current = group_target var current = group_target
for (var e = 0; e < length(eg.effects); e++) { var e = 0
var effect = eg.effects[e] var effect = null
current = apply_effect(ctx, effect, current, res, camera, gname, plane_name, group_effects) 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 // 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 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') { if (effect.type == 'bloom') {
var bright = ctx.alloc(size.width, size.height, hint + '_bright') bright = ctx.alloc(size.width, size.height, hint + '_bright')
var blur1 = ctx.alloc(size.width, size.height, hint + '_blur1') blur1 = ctx.alloc(size.width, size.height, hint + '_blur1')
var blur2 = ctx.alloc(size.width, size.height, hint + '_blur2') blur2 = ctx.alloc(size.width, size.height, hint + '_blur2')
// Threshold // Threshold
push(ctx.passes, { push(ctx.passes, {
type: 'shader_pass', type: 'shader_pass',
@@ -192,11 +220,11 @@ function apply_effect(ctx, effect, input, size, camera, hint, current_plane, gro
output: bright, output: bright,
uniforms: {threshold: effect.threshold || 0.8, intensity: effect.intensity || 1} uniforms: {threshold: effect.threshold || 0.8, intensity: effect.intensity || 1}
}) })
// Blur passes // Blur passes
var blur_passes = effect.blur_passes || 2 blur_passes = effect.blur_passes || 2
var blur_in = bright blur_in = bright
for (var p = 0; p < blur_passes; p++) { 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: 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}}}) 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 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'}) push(ctx.passes, {type: 'composite_textures', base: input, overlay: blur2, output: output, mode: 'add'})
} else if (effect.type == 'mask') { } 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 // 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) { 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 // Render mask
push(ctx.passes, { push(ctx.passes, {
@@ -261,17 +289,25 @@ compositor.execute = function(plan) {
} }
var commands = [] var commands = []
for (var i = 0; i < length(plan.passes); i++) { var i = 0
var pass = plan.passes[i] 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') { 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: 'begin_render', target: target, clear: pass.color})
push(commands, {cmd: 'end_render'}) push(commands, {cmd: 'end_render'})
} else if (pass.type == 'render') { } else if (pass.type == 'render') {
var result = film2d.render({ result = film2d.render({
drawables: pass.drawables, drawables: pass.drawables,
camera: pass.camera, camera: pass.camera,
target: resolve(pass.target), target: resolve(pass.target),
@@ -279,7 +315,7 @@ compositor.execute = function(plan) {
layer_sort: pass.layer_sort || {}, layer_sort: pass.layer_sort || {},
clear: pass.clear clear: pass.clear
}, backend) }, backend)
for (var c = 0; c < length(result.commands); c++) for (c = 0; c < length(result.commands); c++)
push(commands, result.commands[c]) push(commands, result.commands[c])
} else if (pass.type == 'shader_pass') { } else if (pass.type == 'shader_pass') {
@@ -319,7 +355,7 @@ compositor.execute = function(plan) {
}) })
} else if (pass.type == 'blit_to_screen') { } 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, { push(commands, {
cmd: 'blit', cmd: 'blit',
texture: resolve(pass.source), texture: resolve(pass.source),
@@ -328,8 +364,8 @@ compositor.execute = function(plan) {
filter: pass.presentation == 'integer_scale' ? 'nearest' : 'linear' filter: pass.presentation == 'integer_scale' ? 'nearest' : 'linear'
}) })
} else if (pass.type == 'blit') { } else if (pass.type == 'blit') {
var src = resolve(pass.source) src = resolve(pass.source)
var dst = resolve(pass.dest) dst = resolve(pass.dest)
push(commands, { push(commands, {
cmd: 'blit', cmd: 'blit',
texture: src, texture: src,
@@ -351,20 +387,26 @@ compositor.execute = function(plan) {
function _calc_presentation(src, dst, mode) { function _calc_presentation(src, dst, mode) {
if (mode == 'stretch') if (mode == 'stretch')
return {x: 0, y: 0, width: dst.width, height: dst.height} 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') { if (mode == 'integer_scale') {
var sx = floor(dst.width / src.width) sx = floor(dst.width / src.width)
var sy = floor(dst.height / src.height) sy = floor(dst.height / src.height)
var s = max(1, min(sx, sy)) s = max(1, min(sx, sy))
var w = src.width * s w = src.width * s
var h = src.height * s h = src.height * s
return {x: (dst.width - w) / 2, y: (dst.height - h) / 2, width: w, height: h} return {x: (dst.width - w) / 2, y: (dst.height - h) / 2, width: w, height: h}
} }
// letterbox // letterbox
var scale = min(dst.width / src.width, dst.height / src.height) scale = min(dst.width / src.width, dst.height / src.height)
var w = src.width * scale w = src.width * scale
var h = src.height * scale h = src.height * scale
return {x: (dst.width - w) / 2, y: (dst.height - h) / 2, width: w, height: h} return {x: (dst.width - w) / 2, y: (dst.height - h) / 2, width: w, height: h}
} }

16
core.cm
View File

@@ -84,11 +84,12 @@ var _fps_sample_pos = 0
function fps_add_sample(sample) { function fps_add_sample(sample) {
var n = length(_fps_samples) var n = length(_fps_samples)
var old = null
if (n < _fps_sample_count) { if (n < _fps_sample_count) {
push(_fps_samples, sample) push(_fps_samples, sample)
_fps_sample_sum += sample _fps_sample_sum += sample
} else { } else {
var old = _fps_samples[_fps_sample_pos] old = _fps_samples[_fps_sample_pos]
_fps_samples[_fps_sample_pos] = sample _fps_samples[_fps_sample_pos] = sample
_fps_sample_sum += sample - old _fps_sample_sum += sample - old
_fps_sample_pos++ _fps_sample_pos++
@@ -165,22 +166,25 @@ function _main_loop() {
} }
// Render // Render
var render_result = null
var dbg = false
var stats = null
if (_config.render) { if (_config.render) {
var render_result = _config.render() render_result = _config.render()
if (render_result) { if (render_result) {
if (_config.debug == 'graph') { if (_config.debug == 'graph') {
log.console(render_result) log.console(render_result)
$stop() $stop()
return return
} }
var dbg = _config.debug == 'cmd' dbg = _config.debug == 'cmd'
// Build stats for debug_imgui // Build stats for debug_imgui
var stats = { stats = {
fps: _current_fps, fps: _current_fps,
frame_time_ms: _frame_time_ms frame_time_ms: _frame_time_ms
} }
// Handle both compositor result ({commands: [...]}) and fx_graph (graph object) // Handle both compositor result ({commands: [...]}) and fx_graph (graph object)
if (render_result.commands) { if (render_result.commands) {
if (_config.imgui || _config.editor) { if (_config.imgui || _config.editor) {

View File

@@ -117,14 +117,15 @@ function _render_node_tree(imgui, node, depth) {
} }
var has_children = node.children && length(node.children) > 0 var has_children = node.children && length(node.children) > 0
if (has_children) { if (has_children) {
imgui.tree(label, function() { imgui.tree(label, function() {
// Show node summary // Show node summary
_render_node_summary(imgui, node) _render_node_summary(imgui, node)
// Recurse children // 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) _render_node_tree(imgui, node.children[i], depth + 1)
} }
}) })
@@ -154,16 +155,18 @@ function _render_node_summary(imgui, node) {
push(info, "img:" + node.image) push(info, "img:" + node.image)
} }
var t = null
if (node.text) { if (node.text) {
var t = node.text t = node.text
if (length(t) > 20) t = text(t, 0, 17) + "..." if (length(t) > 20) t = text(t, 0, 17) + "..."
push(info, "\"" + t + "\"") push(info, "\"" + t + "\"")
} }
var fx = []
var j = 0
if (node.effects && length(node.effects) > 0) { if (node.effects && length(node.effects) > 0) {
var fx = [] for (j = 0; j < length(node.effects); j++) {
for (var i = 0; i < length(node.effects); i++) { push(fx, node.effects[j].type)
push(fx, node.effects[i].type)
} }
push(info, "fx:[" + text(fx, ",") + "]") push(info, "fx:[" + text(fx, ",") + "]")
} }
@@ -189,9 +192,10 @@ function _render_node_inspector(imgui, node) {
imgui.text("---") imgui.text("---")
// Position // Position
var pos = null
if (node.pos) { if (node.pos) {
imgui.text("Position") 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 if (pos != node.pos.x) node.pos.x = pos
pos = imgui.slider("Y", node.pos.y, -1000, 1000) pos = imgui.slider("Y", node.pos.y, -1000, 1000)
if (pos != node.pos.y) node.pos.y = pos if (pos != node.pos.y) node.pos.y = pos
@@ -242,15 +246,17 @@ function _render_node_inspector(imgui, node) {
} }
// Effects // Effects
var ei = 0
var fx = null
if (node.effects && length(node.effects) > 0) { if (node.effects && length(node.effects) > 0) {
imgui.text("---") imgui.text("---")
imgui.text("Effects:") imgui.text("Effects:")
for (var i = 0; i < length(node.effects); i++) { for (ei = 0; ei < length(node.effects); ei++) {
var fx = node.effects[i] fx = node.effects[ei]
imgui.tree(fx.type, function() { imgui.tree(fx.type, function() {
arrfor(array(fx), k => { arrfor(array(fx), k => {
var v = fx[k]
if (k != 'type' && k != 'source') { if (k != 'type' && k != 'source') {
var v = fx[k]
if (is_number(v)) { if (is_number(v)) {
fx[k] = imgui.slider(k, v, 0, 10) fx[k] = imgui.slider(k, v, 0, 10)
} else { } else {
@@ -288,21 +294,25 @@ function _render_graph_view(imgui, plan) {
imgui.text("Persistent: " + text(length(array(plan.persistent_targets || {})))) imgui.text("Persistent: " + text(length(array(plan.persistent_targets || {}))))
imgui.text("---") imgui.text("---")
for (var i = 0; i < length(plan.passes); i++) { var i = 0
var pass = plan.passes[i] var pass = null
var label = text(i) + ": " + pass.type 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.shader) label += " [" + pass.shader + "]"
if (pass.renderer) label += " [" + pass.renderer + "]" if (pass.renderer) label += " [" + pass.renderer + "]"
if (imgui.button(label)) { if (imgui.button(label)) {
_selected_pass = pass _selected_pass = pass
} }
// Show target info // Show target info
imgui.sameline(0) imgui.sameline(0)
var target_info = "" target_info = ""
if (pass.target) { if (pass.target) {
if (pass.target == 'screen') { if (pass.target == 'screen') {
target_info = "-> screen" target_info = "-> screen"
@@ -397,14 +407,17 @@ function _render_effects_panel(imgui) {
imgui.text("Registered effects: " + text(length(effect_list))) imgui.text("Registered effects: " + text(length(effect_list)))
imgui.text("---") imgui.text("---")
for (var i = 0; i < length(effect_list); i++) { var i = 0
var name = effect_list[i] var name = null
var deff = effects_mod.get(name) 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.tree(name, function() {
imgui.text("Type: " + (deff.type || 'unknown')) imgui.text("Type: " + (deff.type || 'unknown'))
imgui.text("Requires target: " + (deff.requires_target ? "yes" : "no")) imgui.text("Requires target: " + (deff.requires_target ? "yes" : "no"))
if (deff.params) { if (deff.params) {
imgui.text("Parameters:") imgui.text("Parameters:")
arrfor(array(deff.params), k => { arrfor(array(deff.params), k => {

20
ease.cm
View File

@@ -66,13 +66,19 @@ Ease.bounce = {
out(t) { out(t) {
var n1 = 7.5625 var n1 = 7.5625
var d1 = 2.75 var d1 = 2.75
if (t < 1 / d1) { var u = t
return n1 * t * t if (u < 1 / d1) {
} else if (t < 2 / d1) { return n1 * u * u
return n1 * (t -= 1.5 / d1) * t + 0.75 } else if (u < 2 / d1) {
} else if (t < 2.5 / d1) { u = u - 1.5 / d1
return n1 * (t -= 2.25 / d1) * t + 0.9375 return n1 * u * u + 0.75
} else return n1 * (t -= 2.625 / d1) * t + 0.984375 } 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) { inout(t) {
return t < 0.5 ? (1 - this.out(1 - 2 * t)) / 2 : (1 + this.out(2 * t - 1)) / 2 return t < 0.5 ? (1 - this.out(1 - 2 * t)) / 2 : (1 + this.out(2 * t - 1)) / 2

View File

@@ -53,8 +53,9 @@ effects.register('bloom', {
var blur_src = thresh_target var blur_src = thresh_target
var texel = {x: 1 / size.width, y: 1 / size.height} var texel = {x: 1 / size.width, y: 1 / size.height}
var blur_count = params.blur_passes != null ? params.blur_passes : 3 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, { push(passes, {
type: 'shader', type: 'shader',
shader: 'blur', shader: 'blur',
@@ -188,8 +189,9 @@ effects.register('blur', {
var blur_b = ctx.alloc_target(size.width, size.height, 'blur_b') var blur_b = ctx.alloc_target(size.width, size.height, 'blur_b')
var src = input var src = input
var blur_count = params.passes != null ? params.passes : 2 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, { push(passes, {
type: 'shader', type: 'shader',
shader: 'blur', shader: 'blur',

View File

@@ -38,41 +38,28 @@ action.on_input = function(action_id, action_data)
emacs_notation += lower(key) emacs_notation += lower(key)
} else { } else {
// Handle special keys // Handle special keys
switch (key) { if (key == 'return' || key == 'enter') {
case 'return': emacs_notation += "RET"
case 'enter': } else if (key == 'space') {
emacs_notation += "RET" emacs_notation += "SPC"
break } else if (key == 'escape') {
case 'space': emacs_notation += "ESC"
emacs_notation += "SPC" } else if (key == 'tab') {
break emacs_notation += "TAB"
case 'escape': } else if (key == 'backspace') {
emacs_notation += "ESC" emacs_notation += "DEL"
break } else if (key == 'delete') {
case 'tab': emacs_notation += "delete"
emacs_notation += "TAB" } else if (key == 'up') {
break emacs_notation += "up"
case 'backspace': } else if (key == 'down') {
emacs_notation += "DEL" emacs_notation += "down"
break } else if (key == 'left') {
case 'delete': emacs_notation += "left"
emacs_notation += "delete" } else if (key == 'right') {
break emacs_notation += "right"
case 'up': } else {
emacs_notation += "up" emacs_notation += key
break
case 'down':
emacs_notation += "down"
break
case 'left':
emacs_notation += "left"
break
case 'right':
emacs_notation += "right"
break
default:
emacs_notation += key
break
} }
} }
@@ -81,10 +68,11 @@ action.on_input = function(action_id, action_data)
this.prefix_key = emacs_notation this.prefix_key = emacs_notation
return return
} }
// If we have a prefix key, build the full command // If we have a prefix key, build the full command
var full_command = null
if (this.prefix_key) { 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 this.prefix_key = null // Reset prefix key
// scene.recurse(game.root, 'on_input', [full_command, action_data]) // scene.recurse(game.root, 'on_input', [full_command, action_data])
} else { } else {

View File

@@ -13,7 +13,8 @@ var bunnyTex = graphics.texture("bunny")
var bunnies = [] var bunnies = []
// Start with some initial bunnies: // Start with some initial bunnies:
for (var i = 0; i < 100; i++) { var i = 0;
for (i = 0; i < 100; i++) {
push(bunnies, { push(bunnies, {
x: random.random() * config.width, x: random.random() * config.width,
y: random.random() * config.height, y: random.random() * config.height,
@@ -25,8 +26,10 @@ for (var i = 0; i < 100; i++) {
this.update = function(dt) { this.update = function(dt) {
// If left mouse is down, spawn some more bunnies: // If left mouse is down, spawn some more bunnies:
var mouse = input.mousestate() var mouse = input.mousestate()
var i = 0;
var b = null;
if (mouse.left) if (mouse.left)
for (var i = 0; i < 50; i++) { for (i = 0; i < 50; i++) {
push(bunnies, { push(bunnies, {
x: mouse.x, x: mouse.x,
y: mouse.y, y: mouse.y,
@@ -36,8 +39,8 @@ this.update = function(dt) {
} }
// Update bunny positions and bounce them inside the screen: // Update bunny positions and bounce them inside the screen:
for (var i = 0; i < length(bunnies); i++) { for (i = 0; i < length(bunnies); i++) {
var b = bunnies[i] b = bunnies[i]
b.x += b.vx * dt b.x += b.vx * dt
b.y += b.vy * dt b.y += b.vy * dt

View File

@@ -28,26 +28,21 @@ var isMyTurn = false;
function updateTitle() { function updateTitle() {
var title = "Misty Chess - "; var title = "Misty Chess - ";
switch(gameState) { if (gameState == 'waiting') {
case 'waiting': title += "Press S to start server or J to join";
title += "Press S to start server or J to join"; } else if (gameState == 'searching') {
break; title += "Searching for server...";
case 'searching': } else if (gameState == 'server_waiting') {
title += "Searching for server..."; title += "Waiting for player to join...";
break; } else if (gameState == 'connected') {
case 'server_waiting': if (myColor) {
title += "Waiting for player to join..."; title += (mover.turn == myColor ? "Your turn (" + myColor + ")" : "Opponent's turn (" + mover.turn + ")");
break; } else {
case 'connected': title += mover.turn + " turn";
if (myColor) { }
title += (mover.turn == myColor ? "Your turn (" + myColor + ")" : "Opponent's turn (" + mover.turn + ")");
} else {
title += mover.turn + " turn";
}
break;
} }
log.console(title) 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 ──────────────────────────────────── */ /* ── draw one 8×8 chess board ──────────────────────────────────── */
function drawBoard() { function drawBoard() {
for (var y = 0; y < 8; ++y) var y = 0;
for (var x = 0; x < 8; ++x) { var x = 0;
var isMyHover = hoverPos && hoverPos[0] == x && hoverPos[1] == y; var isMyHover = null;
var isOpponentHover = opponentMousePos && opponentMousePos[0] == x && opponentMousePos[1] == y; var isOpponentHover = null;
var isValidMove = selectPos && holdingPiece && isValidMoveForTurn(selectPos, [x, y]); var isValidMove = null;
var color = null;
var color = ((x+y)&1) ? dark : light; 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) { if (isValidMove) {
color = allowedColor; // Gold for allowed moves color = allowedColor; // Gold for allowed moves
@@ -220,47 +221,51 @@ function isValidMoveForTurn(from, to) {
/* ── draw every live piece ─────────────────────────────────────── */ /* ── draw every live piece ─────────────────────────────────────── */
function drawPieces() { function drawPieces() {
grid.each(function (piece) { var piece = null;
if (piece.captured) return; var r = null;
var opponentPiece = null;
grid.each(function (p) {
if (p.captured) return;
// Skip drawing the piece being held (by me or opponent) // Skip drawing the piece being held (by me or opponent)
if (holdingPiece && selectPos && if (holdingPiece && selectPos &&
piece.coord[0] == selectPos[0] && p.coord[0] == selectPos[0] &&
piece.coord[1] == selectPos[1]) { p.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]) {
return; 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 }; 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 // Draw the held piece at the mouse position if we're holding one
if (holdingPiece && selectPos && hoverPos) { if (holdingPiece && selectPos && hoverPos) {
var piece = grid.at(selectPos)[0]; piece = grid.at(selectPos)[0];
if (piece) { 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 }; width:S, height:S };
draw2d.image(piece.sprite, r); draw2d.image(piece.sprite, r);
} }
} }
// Draw opponent's held piece if they're dragging one // Draw opponent's held piece if they're dragging one
if (opponentHoldingPiece && opponentSelectPos && opponentMousePos) { if (opponentHoldingPiece && opponentSelectPos && opponentMousePos) {
var opponentPiece = grid.at(opponentSelectPos)[0]; opponentPiece = grid.at(opponentSelectPos)[0];
if (opponentPiece) { 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 }; width:S, height:S };
// Draw with slight transparency to show it's the opponent's piece // Draw with slight transparency to show it's the opponent's piece
draw2d.image(opponentPiece.sprite, r); draw2d.image(opponentPiece.sprite, r);
} }
@@ -325,13 +330,16 @@ function joinServer() {
} }
$receiver(e => { $receiver(e => {
var fromCell = null;
var piece = null;
if (e.kind == 'update') if (e.kind == 'update')
send(e, update(e.dt)) send(e, update(e.dt))
else if (e.kind == 'draw') else if (e.kind == 'draw')
send(e, draw()) send(e, draw())
else if (e.type == 'game_start' || e.type == 'move' || e.type == 'greet') else if (e.type == 'game_start' || e.type == 'move' || e.type == 'greet')
log.console("Receiver got message:", e.type, e); log.console("Receiver got message:", e.type, e);
if (e.type == 'greet') { if (e.type == 'greet') {
log.console("Server received greet from client"); log.console("Server received greet from client");
// Store the client's actor object for ongoing communication // Store the client's actor object for ongoing communication
@@ -339,7 +347,7 @@ $receiver(e => {
log.console("Stored client actor:", opponent); log.console("Stored client actor:", opponent);
gameState = 'connected'; gameState = 'connected';
updateTitle(); updateTitle();
// Send game_start to the client // Send game_start to the client
log.console("Sending game_start to client"); log.console("Sending game_start to client");
send(opponent, { send(opponent, {
@@ -357,9 +365,9 @@ $receiver(e => {
} else if (e.type == 'move') { } else if (e.type == 'move') {
log.console("Received move from opponent:", e.from, "to", e.to); log.console("Received move from opponent:", e.from, "to", e.to);
// Apply opponent's move // Apply opponent's move
var fromCell = grid.at(e.from); fromCell = grid.at(e.from);
if (length(fromCell)) { if (length(fromCell)) {
var piece = fromCell[0]; piece = fromCell[0];
if (mover.tryMove(piece, e.to)) { if (mover.tryMove(piece, e.to)) {
isMyTurn = true; // It's now our turn isMyTurn = true; // It's now our turn
updateTitle(); updateTitle();

View File

@@ -2,11 +2,13 @@ function grid(w, h) {
var newgrid = meme(grid_prototype) var newgrid = meme(grid_prototype)
newgrid.width = w; newgrid.width = w;
newgrid.height = h; newgrid.height = h;
// create a height×width array of empty lists // create a height*width array of empty lists
newgrid.cells = array(h); 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); 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 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 // call fn(entity, coord) for every entity in every cell
each(fn) { each(fn) {
for (var y = 0; y < this.height; y++) { var list = null;
for (var x = 0; x < this.width; x++) { var y = 0;
def list = this.cells[y][x] 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) { arrfor(list, function(entity) {
fn(entity, entity.coord); fn(entity, entity.coord);
}) })
@@ -57,9 +62,11 @@ var grid_prototype = {
// printable representation // printable representation
toString() { toString() {
var out = `grid [${this.width}×${this.height}]\n`; var out = `grid [${this.width}x${this.height}]\n`;
for (var y = 0; y < this.height; y++) { var y = 0;
for (var x = 0; x < this.width; x++) { var x = 0;
for (y = 0; y < this.height; y++) {
for (x = 0; x < this.width; x++) {
out += length(this.cells[y][x]); out += length(this.cells[y][x]);
} }
if (y != this.height - 1) out += "\n"; if (y != this.height - 1) out += "\n";

View File

@@ -10,9 +10,9 @@ var MovementSystem_prototype = {
tryMove: function (piece, to) { tryMove: function (piece, to) {
if (piece.colour != this.turn) return false; if (piece.colour != this.turn) return false;
// normalise to into our hybrid coord // normalise 'to' into our hybrid coord
var dest = [to.x ?? t[0], var dest = [!is_null(to.x) ? to.x : to[0],
to.y ?? to[1]]; !is_null(to.y) ? to.y : to[1]];
if (!this.grid.inBounds(dest)) return false; if (!this.grid.inBounds(dest)) return false;
if (!this.rules.canMove(piece, piece.coord, dest, this.grid)) return false; if (!this.rules.canMove(piece, piece.coord, dest, this.grid)) return false;

View File

@@ -10,7 +10,7 @@ function Piece(kind, colour) {
} }
function startingPosition(grid) { function startingPosition(grid) {
var W = 'white', B = 'black', x; var W = 'white', B = 'black', x = 0;
// pawns // pawns
for (x = 0; x < 8; x++) { for (x = 0; x < 8; x++) {

View File

@@ -1,30 +1,30 @@
/* helper robust coord access */ /* helper -- robust coord access */
function cx(c) { return c.x ?? c[0] } function cx(c) { return !is_null(c.x) ? c.x : c[0] }
function cy(c) { return c.y ?? c[1] } function cy(c) { return !is_null(c.y) ? c.y : c[1] }
/* simple move-shape checks */ /* simple move-shape checks */
var deltas = { var deltas = {
pawn: function (pc, dx, dy, grid, to) { pawn: function (pc, dx, dy, ctx) {
var dir = (pc.colour == 'white') ? -1 : 1; var dir = (pc.colour == 'white') ? -1 : 1;
var base = (pc.colour == 'white') ? 6 : 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 && 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(ctx.grid.at({ x: cx(pc.coord), y: cy(pc.coord)+dir })) == 0 &&
length(grid.at(to)) == 0); length(ctx.grid.at(ctx.to)) == 0);
var cap = (dy == dir && Math.abs(dx) == 1 && length(grid.at(to))); var cap = (dy == dir && abs(dx) == 1 && length(ctx.grid.at(ctx.to)));
return one || two || cap; return one || two || cap;
}, },
rook : function (pc, dx, dy) { return (dx == 0 || dy == 0); }, rook : function (pc, dx, dy) { return (dx == 0 || dy == 0); },
bishop: function (pc, dx, dy) { return Math.abs(dx) == Math.abs(dy); }, bishop: function (pc, dx, dy) { return abs(dx) == abs(dy); },
queen : function (pc, dx, dy) { return (dx == 0 || dy == 0 || Math.abs(dx) == Math.abs(dy)); }, queen : function (pc, dx, dy) { return (dx == 0 || dy == 0 || abs(dx) == abs(dy)); },
knight: function (pc, dx, dy) { return (Math.abs(dx) == 1 && Math.abs(dy) == 2) || knight: function (pc, dx, dy) { return (abs(dx) == 1 && abs(dy) == 2) ||
(Math.abs(dx) == 2 && Math.abs(dy) == 1); }, (abs(dx) == 2 && abs(dy) == 1); },
king : function (pc, dx, dy) { return Math.max(Math.abs(dx), Math.abs(dy)) == 1; } king : function (pc, dx, dy) { return max(abs(dx), abs(dy)) == 1; }
}; };
function clearLine(from, to, grid) { function clearLine(from, to, grid) {
var dx = Math.sign(cx(to) - cx(from)); var dx = sign(cx(to) - cx(from));
var dy = Math.sign(cy(to) - cy(from)); var dy = sign(cy(to) - cy(from));
var x = cx(from) + dx, y = cy(from) + dy; var x = cx(from) + dx, y = cy(from) + dy;
while (x != cx(to) || y != cy(to)) { while (x != cx(to) || y != cy(to)) {
if (length(grid.at({ x: x, y: y }))) return false; 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 dx = cx(to) - cx(from);
var dy = cy(to) - cy(from); var dy = cy(to) - cy(from);
var f = deltas[piece.kind]; 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; if (piece.kind == 'knight') return true;
return clearLine(from, to, grid); return clearLine(from, to, grid);
} }

View File

@@ -13,7 +13,7 @@ var cellSize = 20
var gridW = floor(config.width / cellSize) var gridW = floor(config.width / cellSize)
var gridH = floor(config.height / 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 moveInterval = 0.1
var moveTimer = 0 var moveTimer = 0
var gameState = "playing" var gameState = "playing"
@@ -36,7 +36,8 @@ function resetGame() {
function spawnApple() { function spawnApple() {
apple = {x:floor(random.random()*gridW), y:floor(random.random()*gridH)} apple = {x:floor(random.random()*gridW), y:floor(random.random()*gridH)}
// Re-spawn if apple lands on snake // Re-spawn if apple lands on snake
for (var i=0; i<length(snake); i++) var i = 0;
for (i=0; i<length(snake); i++)
if (snake[i].x == apple.x && snake[i].y == apple.y) { spawnApple(); return } if (snake[i].x == apple.x && snake[i].y == apple.y) { spawnApple(); return }
} }
@@ -63,7 +64,8 @@ this.update = function(dt) {
wrap(head) wrap(head)
// Check collision with body // Check collision with body
for (var i=0; i<length(snake); i++) { var i = 0;
for (i=0; i<length(snake); i++) {
if (snake[i].x == head.x && snake[i].y == head.y) { if (snake[i].x == head.x && snake[i].y == head.y) {
gameState = "gameover" gameState = "gameover"
return return
@@ -81,10 +83,13 @@ this.update = function(dt) {
this.hud = function() { this.hud = function() {
// Optional clear screen // Optional clear screen
draw.rectangle({x:0, y:0, width:config.width, height:config.height}, [0,0,0,1]) draw.rectangle({x:0, y:0, width:config.width, height:config.height}, [0,0,0,1])
// Draw snake // Draw snake
for (var i=0; i<length(snake); i++) { var i = 0;
var s = snake[i] var s = null;
var msg = null;
for (i=0; i<length(snake); i++) {
s = snake[i]
draw.rectangle({x:s.x*cellSize, y:s.y*cellSize, width:cellSize, height:cellSize}, color.green) draw.rectangle({x:s.x*cellSize, y:s.y*cellSize, width:cellSize, height:cellSize}, color.green)
} }
@@ -92,7 +97,7 @@ this.hud = function() {
draw.rectangle({x:apple.x*cellSize, y:apple.y*cellSize, width:cellSize, height:cellSize}, color.red) draw.rectangle({x:apple.x*cellSize, y:apple.y*cellSize, width:cellSize, height:cellSize}, color.red)
if (gameState == "gameover") { if (gameState == "gameover") {
var msg = "GAME OVER! Press SPACE to restart." msg = "GAME OVER! Press SPACE to restart."
draw.text(msg, {x:0, y:config.height*0.5-10, width:config.width, height:20}, null, 0, color.white) draw.text(msg, {x:0, y:config.height*0.5-10, width:config.width, height:20}, null, 0, color.white)
} }
} }

View File

@@ -79,7 +79,7 @@ function unlock_achievement(achievement_name) {
function update_stat(stat_name, value, is_float) { function update_stat(stat_name, value, is_float) {
if (!steam_available || !stats_loaded) return false; if (!steam_available || !stats_loaded) return false;
var success; var success = null;
if (is_float) { if (is_float) {
success = steam.stats.stats_set_float(stat_name, value); success = steam.stats.stats_set_float(stat_name, value);
} else { } else {
@@ -145,7 +145,7 @@ function end_game(score) {
function save_to_cloud(save_data) { function save_to_cloud(save_data) {
if (!steam_available) return false; if (!steam_available) return false;
var json_data = JSON.stringify(save_data); var json_data = json.encode(save_data);
return steam.cloud.cloud_write("savegame.json", json_data); return steam.cloud.cloud_write("savegame.json", json_data);
} }
@@ -153,9 +153,10 @@ function load_from_cloud() {
if (!steam_available) return null; if (!steam_available) return null;
var data = steam.cloud.cloud_read("savegame.json"); var data = steam.cloud.cloud_read("savegame.json");
var json_str = null;
if (data) { if (data) {
var json_str = text(data) json_str = text(data)
return JSON.parse(json_str); return json.decode(json_str);
} }
return null; return null;
@@ -169,17 +170,5 @@ function cleanup_steam() {
} }
} }
// Export the API // Initialize on start
module.exports = { init_steam();
init: init_steam,
update: update_steam,
cleanup: cleanup_steam,
unlock_achievement: unlock_achievement,
update_stat: update_stat,
get_stat: get_stat,
start_game: start_game,
end_game: end_game,
save_to_cloud: save_to_cloud,
load_from_cloud: load_from_cloud,
is_available: function() { return steam_available; }
};

View File

@@ -56,9 +56,12 @@ var shapeKeys = array(SHAPES)
// Initialize board with empty (0) // Initialize board with empty (0)
function initBoard() { function initBoard() {
board = [] board = []
for (var r=0; r<ROWS; r++) { var r = 0;
var row = [] var row = null;
for (var c=0; c<COLS; c++) push(row, 0) var c = 0;
for (r=0; r<ROWS; r++) {
row = []
for (c=0; c<COLS; c++) push(row, 0)
push(board, row) push(board, row)
} }
} }
@@ -66,7 +69,7 @@ initBoard()
function randomShape() { function randomShape() {
var key = shapeKeys[floor(random.random()*length(shapeKeys))] var key = shapeKeys[floor(random.random()*length(shapeKeys))]
// Make a copy of the shapes blocks // Make a copy of the shape's blocks
return { return {
type: key, type: key,
color: SHAPES[key].color, color: SHAPES[key].color,
@@ -84,9 +87,12 @@ function spawnPiece() {
} }
function collides(px, py, blocks) { function collides(px, py, blocks) {
for (var i=0; i<length(blocks); i++) { var i = 0;
var x = px + blocks[i][0] var x = 0;
var y = py + blocks[i][1] var y = 0;
for (i=0; i<length(blocks); i++) {
x = px + blocks[i][0]
y = py + blocks[i][1]
if (x<0 || x>=COLS || y<0 || y>=ROWS) return true if (x<0 || x>=COLS || y<0 || y>=ROWS) return true
if (y>=0 && board[y][x]) return true if (y>=0 && board[y][x]) return true
} }
@@ -95,9 +101,12 @@ function collides(px, py, blocks) {
// Lock piece into board // Lock piece into board
function lockPiece() { function lockPiece() {
for (var i=0; i<length(piece.blocks); i++) { var i = 0;
var x = pieceX + piece.blocks[i][0] var x = 0;
var y = pieceY + piece.blocks[i][1] var y = 0;
for (i=0; i<length(piece.blocks); i++) {
x = pieceX + piece.blocks[i][0]
y = pieceY + piece.blocks[i][1]
if (y>=0) board[y][x] = piece.color if (y>=0) board[y][x] = piece.color
} }
} }
@@ -105,9 +114,12 @@ function lockPiece() {
// Rotate 90° clockwise // Rotate 90° clockwise
function rotate(blocks) { function rotate(blocks) {
// (x,y) => (y,-x) // (x,y) => (y,-x)
for (var i=0; i<length(blocks); i++) { var i = 0;
var x = blocks[i][0] var x = 0;
var y = blocks[i][1] var y = 0;
for (i=0; i<length(blocks); i++) {
x = blocks[i][0]
y = blocks[i][1]
blocks[i][0] = y blocks[i][0] = y
blocks[i][1] = -x blocks[i][1] = -x
} }
@@ -115,14 +127,17 @@ function rotate(blocks) {
function clearLines() { function clearLines() {
var lines = 0 var lines = 0
for (var r=ROWS-1; r>=0;) { var r = ROWS-1;
var newRow = null;
var c = 0;
for (r=ROWS-1; r>=0;) {
if (every(board[r], cell => cell)) { if (every(board[r], cell => cell)) {
lines++ lines++
// remove row // remove row
board = array(array(board, 0, r), array(board, r+1)) board = array(array(board, 0, r), array(board, r+1))
// add empty row on top // add empty row on top
var newRow = [] newRow = []
for (var c=0; c<COLS; c++) push(newRow, 0) for (c=0; c<COLS; c++) push(newRow, 0)
board.unshift(newRow) board.unshift(newRow)
} else { } else {
r-- r--
@@ -195,10 +210,11 @@ this.update = function(dt) {
// ======= End Horizontal Movement Gate ======= // ======= End Horizontal Movement Gate =======
// Rotate with W (once per press, no spinning) // Rotate with W (once per press, no spinning)
var test = null;
if (input.keyboard.down('w')) { if (input.keyboard.down('w')) {
if (!rotateHeld) { if (!rotateHeld) {
rotateHeld = true rotateHeld = true
var test = array(piece.blocks, b => [b[0], b[1]]) test = array(piece.blocks, b => [b[0], b[1]])
rotate(test) rotate(test)
if (!collides(pieceX, pieceY, test)) piece.blocks = 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.rectangle({x:0, y:0, width:config.width, height:config.height}, [0,0,0,1])
// Draw board // Draw board
for (var r=0; r<ROWS; r++) { var r = 0;
for (var c=0; c<COLS; c++) { var c = 0;
var cell = board[r][c] var cell = null;
var i = 0;
var x = 0;
var y = 0;
var nx = 0;
var ny = 0;
var dx = 0;
var dy = 0;
for (r=0; r<ROWS; r++) {
for (c=0; c<COLS; c++) {
cell = board[r][c]
if (!cell) continue if (!cell) continue
draw.rectangle({x:c*TILE, y:(ROWS-1-r)*TILE, width:TILE, height:TILE}, cell) draw.rectangle({x:c*TILE, y:(ROWS-1-r)*TILE, width:TILE, height:TILE}, cell)
} }
@@ -242,9 +268,9 @@ this.hud = function() {
// Draw falling piece // Draw falling piece
if (!gameOver && piece) { if (!gameOver && piece) {
for (var i=0; i<length(piece.blocks); i++) { for (i=0; i<length(piece.blocks); i++) {
var x = pieceX + piece.blocks[i][0] x = pieceX + piece.blocks[i][0]
var y = pieceY + piece.blocks[i][1] y = pieceY + piece.blocks[i][1]
draw.rectangle({x:x*TILE, y:(ROWS-1-y)*TILE, width:TILE, height:TILE}, piece.color) draw.rectangle({x:x*TILE, y:(ROWS-1-y)*TILE, width:TILE, height:TILE}, piece.color)
} }
} }
@@ -252,11 +278,11 @@ this.hud = function() {
// Next piece window // Next piece window
draw.text("Next", {x:70, y:5, width:50, height:10}, null, 0, color.white) draw.text("Next", {x:70, y:5, width:50, height:10}, null, 0, color.white)
if (nextPiece) { if (nextPiece) {
for (var i=0; i<length(nextPiece.blocks); i++) { for (i=0; i<length(nextPiece.blocks); i++) {
var nx = nextPiece.blocks[i][0] nx = nextPiece.blocks[i][0]
var ny = nextPiece.blocks[i][1] ny = nextPiece.blocks[i][1]
var dx = 12 + nx dx = 12 + nx
var dy = 16 - ny dy = 16 - ny
draw.rectangle({x:dx*TILE, y:(ROWS-1-dy)*TILE, width:TILE, height:TILE}, nextPiece.color) draw.rectangle({x:dx*TILE, y:(ROWS-1-dy)*TILE, width:TILE, height:TILE}, nextPiece.color)
} }
} }
@@ -269,4 +295,3 @@ this.hud = function() {
draw.text("GAME OVER", {x:10, y:config.height*0.5-5, width:config.width-20, height:20}, null, 0, color.red) draw.text("GAME OVER", {x:10, y:config.height*0.5-5, width:config.width-20, height:20}, null, 0, color.red)
} }
} }

241
film2d.cm
View File

@@ -64,9 +64,9 @@ function _resolve_sprite_fit(sprite) {
return sprite return sprite
} }
var scale = null
if (fit == 'contain') { if (fit == 'contain') {
// Fit inside box, preserve aspect (letterbox) // Fit inside box, preserve aspect (letterbox)
var scale
if (tex_aspect > box_aspect) { if (tex_aspect > box_aspect) {
// Image wider than box - constrain by width // Image wider than box - constrain by width
scale = target_w / tex_w scale = target_w / tex_w
@@ -79,24 +79,35 @@ function _resolve_sprite_fit(sprite) {
return 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') { if (fit == 'cover') {
// Fill box, preserve aspect (crop via UV) // Fill box, preserve aspect (crop via UV)
var fit_ax = sprite.fit_anchor_x != null ? sprite.fit_anchor_x : 0.5 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 fit_ay = sprite.fit_anchor_y != null ? sprite.fit_anchor_y : 0.5
var scale_w = target_w / tex_w scale_w = target_w / tex_w
var scale_h = target_h / tex_h scale_h = target_h / tex_h
var scale = max(scale_w, scale_h) cover_scale = max(scale_w, scale_h)
// Compute visible portion of texture in UV space // Compute visible portion of texture in UV space
var visible_w = target_w / scale visible_w = target_w / cover_scale
var visible_h = target_h / scale visible_h = target_h / cover_scale
// UV rect (0-1 space) // UV rect (0-1 space)
var uv_w = visible_w / tex_w uv_w = visible_w / tex_w
var uv_h = visible_h / tex_h uv_h = visible_h / tex_h
var uv_x = (1 - uv_w) * fit_ax uv_x = (1 - uv_w) * fit_ax
var uv_y = (1 - uv_h) * fit_ay uv_y = (1 - uv_h) * fit_ay
sprite.width = target_w sprite.width = target_w
sprite.height = target_h sprite.height = target_h
@@ -122,12 +133,14 @@ film2d.register = function(drawable) {
// Index by groups (effect routing only) // Index by groups (effect routing only)
var groups = drawable.groups || [] var groups = drawable.groups || []
for (var i = 0; i < length(groups); i++) { var i = 0
var g = groups[i] var g = null
for (i = 0; i < length(groups); i++) {
g = groups[i]
if (!group_index[g]) group_index[g] = [] if (!group_index[g]) group_index[g] = []
push(group_index[g], id) push(group_index[g], id)
} }
return id return id
} }
@@ -135,21 +148,24 @@ film2d.unregister = function(id) {
var id_str = text(id) var id_str = text(id)
var drawable = registry[id_str] var drawable = registry[id_str]
if (!drawable) return if (!drawable) return
// Remove from plane index // Remove from plane index
var plane = drawable.plane || 'default' var plane = drawable.plane || 'default'
var idx = null
if (plane_index[plane]) { if (plane_index[plane]) {
var idx = find(plane_index[plane], id_str) idx = find(plane_index[plane], id_str)
if (idx != null) if (idx != null)
plane_index[plane] = array(array(plane_index[plane], 0, idx), array(plane_index[plane], idx+1)) plane_index[plane] = array(array(plane_index[plane], 0, idx), array(plane_index[plane], idx+1))
} }
// Remove from group indices // Remove from group indices
var groups = drawable.groups || [] var groups = drawable.groups || []
for (var i = 0; i < length(groups); i++) { var i = 0
var g = groups[i] var g = null
for (i = 0; i < length(groups); i++) {
g = groups[i]
if (group_index[g]) { if (group_index[g]) {
var idx = find(group_index[g], id_str) idx = find(group_index[g], id_str)
if (idx != null) if (idx != null)
group_index[g] = array(array(group_index[g], 0, idx), array(group_index[g], idx+1)) 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) { 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]) 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]) 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 // Query by plane and/or group - returns array of drawables
film2d.query = function(selector) { film2d.query = function(selector) {
var result = [] 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) // Query by plane (primary selection)
if (selector.plane) { if (selector.plane) {
var ids = plane_index[selector.plane] || [] ids = plane_index[selector.plane] || []
for (var i = 0; i < length(ids); i++) { for (i = 0; i < length(ids); i++) {
var d = registry[ids[i]] d = registry[ids[i]]
if (d && d.visible != false) { if (d && d.visible != false) {
// If also filtering by group, check membership // If also filtering by group, check membership
if (selector.group) { if (selector.group) {
var groups = d.groups || [] groups = d.groups || []
if (search(groups, selector.group) != null) push(result, d) if (search(groups, selector.group) != null) push(result, d)
} else { } else {
push(result, d) push(result, d)
@@ -203,32 +226,32 @@ film2d.query = function(selector) {
} }
return result return result
} }
// Query by group only (for effect routing) // Query by group only (for effect routing)
if (selector.group) { if (selector.group) {
var ids = group_index[selector.group] || [] ids = group_index[selector.group] || []
for (var i = 0; i < length(ids); i++) { for (i = 0; i < length(ids); i++) {
var d = registry[ids[i]] d = registry[ids[i]]
if (d && d.visible != false) push(result, d) if (d && d.visible != false) push(result, d)
} }
return result return result
} }
if (selector.groups) { if (selector.groups) {
var seen = {} seen = {}
for (var g = 0; g < length(selector.groups); g++) { for (g = 0; g < length(selector.groups); g++) {
var ids = group_index[selector.groups[g]] || [] ids = group_index[selector.groups[g]] || []
for (var i = 0; i < length(ids); i++) { for (i = 0; i < length(ids); i++) {
if (!seen[ids[i]]) { if (!seen[ids[i]]) {
seen[ids[i]] = true seen[ids[i]] = true
var d = registry[ids[i]] d = registry[ids[i]]
if (d && d.visible != false) push(result, d) if (d && d.visible != false) push(result, d)
} }
} }
} }
return result return result
} }
// All drawables // All drawables
var draws = array(registry, id => registry[id]) var draws = array(registry, id => registry[id])
result = array(result, filter(draws, d => d.visible != false)) result = array(result, filter(draws, d => d.visible != false))
@@ -277,10 +300,14 @@ film2d.render = function(params, render_backend) {
// Bucket drawables by layer // Bucket drawables by layer
var buckets = {} var buckets = {}
for (var i = 0; i < length(drawables); i++) { var i = 0
var d = drawables[i] var d = null
var layer_key = text(d.layer) var layer_key = null
var b = buckets[layer_key] var b = null
for (i = 0; i < length(drawables); i++) {
d = drawables[i]
layer_key = text(d.layer)
b = buckets[layer_key]
if (!b) { if (!b) {
b = [] b = []
buckets[layer_key] = b buckets[layer_key] = b
@@ -296,19 +323,23 @@ film2d.render = function(params, render_backend) {
// Merge buckets, y-sorting buckets that request it // Merge buckets, y-sorting buckets that request it
var y_down = camera && camera.y_down == true var y_down = camera && camera.y_down == true
var sorted_drawables = [] var sorted_drawables = []
var li = 0
var mode = null
var keys = null
var j = 0
for (var li = 0; li < length(layers); li++) { for (li = 0; li < length(layers); li++) {
var layer_key = layers[li] layer_key = layers[li]
var b = buckets[layer_key] b = buckets[layer_key]
var mode = layer_sort[layer_key] || "explicit" mode = layer_sort[layer_key] || "explicit"
if (mode == "y") { 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 b = sort(b, keys) // ascending feet-y
if (!y_down) b = reverse(b) // y_up => smaller y draws later => reverse 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 drawables = sorted_drawables
@@ -318,9 +349,10 @@ film2d.render = function(params, render_backend) {
push(commands, { cmd: "set_camera", camera: camera }) push(commands, { cmd: "set_camera", camera: camera })
var batches = _batch_drawables(drawables) var batches = _batch_drawables(drawables)
var batch = null
for (var i = 0; i < length(batches); i++) { for (i = 0; i < length(batches); i++) {
var batch = batches[i] batch = batches[i]
if (batch.type == "sprite_batch") if (batch.type == "sprite_batch")
push(commands, { cmd: "draw_batch", batch_type: "sprites", geometry: { sprites: batch.sprites }, texture: batch.texture, material: batch.material }) push(commands, { cmd: "draw_batch", batch_type: "sprites", geometry: { sprites: batch.sprites }, texture: batch.texture, material: batch.material })
else if (batch.type == "mesh2d_batch") else if (batch.type == "mesh2d_batch")
@@ -341,17 +373,40 @@ function _batch_drawables(drawables) {
var batches = [] var batches = []
var current = null var current = null
var default_mat = {blend: 'alpha', sampler: 'nearest'} var default_mat = {blend: 'alpha', sampler: 'nearest'}
var i = 0
for (var i = 0; i < length(drawables); i++) { var d = null
var d = drawables[i] 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') { if (d.type == 'sprite') {
// Resolve fit mode (computes final width/height/uv_rect) // Resolve fit mode (computes final width/height/uv_rect)
_resolve_sprite_fit(d) _resolve_sprite_fit(d)
var tex = d.texture || d.image tex = d.texture || d.image
var mat = d.material || {blend: 'alpha', sampler: d.filter || 'nearest'} mat = d.material || {blend: 'alpha', sampler: d.filter || 'nearest'}
if (current && current.type == 'sprite_batch' && current.texture == tex && _mat_eq(current.material, mat)) { if (current && current.type == 'sprite_batch' && current.texture == tex && _mat_eq(current.material, mat)) {
push(current.sprites, d) push(current.sprites, d)
} else { } else {
@@ -360,16 +415,16 @@ function _batch_drawables(drawables) {
} }
} else if (d.type == 'particles') { } else if (d.type == 'particles') {
// Convert particles to sprites // Convert particles to sprites
var tex = d.texture || d.image tex = d.texture || d.image
var mat = d.material || default_mat mat = d.material || default_mat
var particles = d.particles || [] particles = d.particles || []
var emitter_opacity = d.opacity != null ? d.opacity : 1 emitter_opacity = d.opacity != null ? d.opacity : 1
var emitter_tint = d.tint || {r: 1, g: 1, b: 1, a: 1} emitter_tint = d.tint || {r: 1, g: 1, b: 1, a: 1}
for (var p = 0; p < length(particles); p++) { for (p = 0; p < length(particles); p++) {
var part = particles[p] part = particles[p]
var pc = part.color || {r: 1, g: 1, b: 1, a: 1} pc = part.color || {r: 1, g: 1, b: 1, a: 1}
var sprite = { sprite = {
type: 'sprite', type: 'sprite',
pos: part.pos, pos: part.pos,
width: (d.width || 16) * (part.scale || 1), width: (d.width || 16) * (part.scale || 1),
@@ -380,7 +435,7 @@ function _batch_drawables(drawables) {
opacity: emitter_opacity, opacity: emitter_opacity,
tint: emitter_tint tint: emitter_tint
} }
if (current && current.type == 'sprite_batch' && current.texture == tex && _mat_eq(current.material, mat)) { if (current && current.type == 'sprite_batch' && current.texture == tex && _mat_eq(current.material, mat)) {
push(current.sprites, sprite) push(current.sprites, sprite)
} else { } else {
@@ -390,25 +445,25 @@ function _batch_drawables(drawables) {
} }
} else if (d.type == 'tilemap') { } else if (d.type == 'tilemap') {
// Expand tilemap to sprites // Expand tilemap to sprites
var tiles = d.tiles || [] tiles = d.tiles || []
var tile_w = d.tile_width || 1 tile_w = d.tile_width || 1
var tile_h = d.tile_height || 1 tile_h = d.tile_height || 1
var off_x = d.offset_x || 0 off_x = d.offset_x || 0
var off_y = d.offset_y || 0 off_y = d.offset_y || 0
var tilemap_opacity = d.opacity != null ? d.opacity : 1 tilemap_opacity = d.opacity != null ? d.opacity : 1
var tilemap_tint = d.tint || {r: 1, g: 1, b: 1, a: 1} tilemap_tint = d.tint || {r: 1, g: 1, b: 1, a: 1}
for (var x = 0; x < length(tiles); x++) { for (x = 0; x < length(tiles); x++) {
if (!tiles[x]) continue if (!tiles[x]) continue
for (var y = 0; y < length(tiles[x]); y++) { for (y = 0; y < length(tiles[x]); y++) {
var img = tiles[x][y] img = tiles[x][y]
if (!img) continue if (!img) continue
var wx = (x + off_x) * tile_w wx = (x + off_x) * tile_w
var wy = (y + off_y) * tile_h wy = (y + off_y) * tile_h
// Center anchor for sprite // Center anchor for sprite
var sprite = { sprite = {
type: 'sprite', type: 'sprite',
image: img, image: img,
pos: {x: wx + tile_w/2, y: wy + tile_h/2}, pos: {x: wx + tile_w/2, y: wy + tile_h/2},
@@ -420,10 +475,10 @@ function _batch_drawables(drawables) {
opacity: tilemap_opacity, opacity: tilemap_opacity,
tint: tilemap_tint tint: tilemap_tint
} }
// Batching // Batching
var tex = img tex = img
var mat = default_mat mat = default_mat
if (current && current.type == 'sprite_batch' && current.texture == tex && _mat_eq(current.material, mat)) { if (current && current.type == 'sprite_batch' && current.texture == tex && _mat_eq(current.material, mat)) {
push(current.sprites, sprite) push(current.sprites, sprite)
} else { } else {
@@ -434,8 +489,8 @@ function _batch_drawables(drawables) {
} }
} else if (d.type == 'mesh2d') { } else if (d.type == 'mesh2d') {
// Mesh2d drawables - arbitrary triangle meshes (for lines, ropes, etc) // Mesh2d drawables - arbitrary triangle meshes (for lines, ropes, etc)
var tex = d.texture || d.image tex = d.texture || d.image
var mat = d.material || {blend: d.blend || 'alpha', sampler: d.filter || 'linear'} 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)) { if (current && current.type == 'mesh2d_batch' && current.texture == tex && _mat_eq(current.material, mat)) {
push(current.meshes, d) push(current.meshes, d)

View File

@@ -60,11 +60,11 @@
var fx_graph = {} var fx_graph = {}
fx_graph.add_node = function(type, params) { fx_graph.add_node = function(type, params) {
params = params || {} var local_params = params || {}
var node = { var node = {
id: this.next_id++, id: this.next_id++,
type: type, type: type,
params: params, params: local_params,
output: {node_id: this.next_id - 1, slot: 'output'} output: {node_id: this.next_id - 1, slot: 'output'}
} }
push(this.nodes, node) push(this.nodes, node)
@@ -137,7 +137,7 @@ NODE_EXECUTORS.render_view = function(params, backend) {
var renderer = params.renderer var renderer = params.renderer
// Determine target // Determine target
var target var target = null
if (target_spec == 'screen') { if (target_spec == 'screen') {
target = 'screen' target = 'screen'
} else if (target_spec && target_spec.texture) { } else if (target_spec && target_spec.texture) {
@@ -257,17 +257,18 @@ NODE_EXECUTORS.clip_rect = function(params, backend) {
// Insert scissor after begin_render // Insert scissor after begin_render
var insert_idx = 0 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') { if (commands[i].cmd == 'begin_render') {
insert_idx = i + 1 insert_idx = i + 1
break break
} }
} }
commands = array(array(array(commands, 0, insert_idx), [{cmd: 'scissor', rect: rect}]), array(commands, insert_idx)) commands = array(array(array(commands, 0, insert_idx), [{cmd: 'scissor', rect: rect}]), array(commands, insert_idx))
// Add scissor reset before end_render // 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') { if (commands[i].cmd == 'end_render') {
commands = array(array(array(commands, 0, i), [{cmd: 'scissor', rect:null}]) ,array(commands, i+1)) commands = array(array(array(commands, 0, i), [{cmd: 'scissor', rect:null}]) ,array(commands, i+1))
break break
@@ -287,7 +288,8 @@ NODE_EXECUTORS.blit = function(params, backend) {
var src_target = input && input.target ? input.target : input var src_target = input && input.target ? input.target : input
if (!src_target) return {target: null, commands: []} if (!src_target) return {target: null, commands: []}
var target var target = null
var key = null
if (target_spec == 'screen') { if (target_spec == 'screen') {
target = 'screen' target = 'screen'
} else if (target_spec && target_spec.target) { } else if (target_spec && target_spec.target) {
@@ -298,7 +300,7 @@ NODE_EXECUTORS.blit = function(params, backend) {
target = target_spec target = target_spec
} else if (target_spec && target_spec.width) { } else if (target_spec && target_spec.width) {
// Target spec - use a consistent key based on the spec itself // 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) target = backend.get_or_create_target(target_spec.width, target_spec.height, key)
} else { } else {
return {target: null, commands: []} return {target: null, commands: []}
@@ -336,16 +338,18 @@ NODE_EXECUTORS.shader_pass = function(params, backend) {
if (!input || !input.target) return {target: null, commands: []} if (!input || !input.target) return {target: null, commands: []}
var src = input.target var src = input.target
var target var target = null
var w = 0
var h = 0
if (output_spec == 'screen') { if (output_spec == 'screen') {
target = 'screen' target = 'screen'
} else if (output_spec && output_spec.texture) { } else if (output_spec && output_spec.texture) {
target = output_spec target = output_spec
} else { } else {
// Default to input size if not specified // Default to input size if not specified
var w = output_spec && output_spec.width ? output_spec.width : src.width w = output_spec && output_spec.width ? output_spec.width : src.width
var h = output_spec && output_spec.height ? output_spec.height : src.height h = output_spec && output_spec.height ? output_spec.height : src.height
target = backend.get_or_create_target(w, h, 'shader_' + shader + '_' + params._node_id) target = backend.get_or_create_target(w, h, 'shader_' + shader + '_' + params._node_id)
} }

View File

@@ -615,7 +615,7 @@ JSC_CCALL(geometry_tilemap_to_data,
JS_FreeValue(js, pos_y_val); JS_FreeValue(js, pos_y_val);
JSValue tiles_array = JS_GetPropertyStr(js, tilemap_obj, "tiles"); 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); JS_FreeValue(js, tiles_array);
return JS_ThrowTypeError(js, "tilemap.tiles must be an 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 tile_count = 0;
int tiles_len = JS_ArrayLength(js, tiles_array); int tiles_len = JS_ArrayLength(js, tiles_array);
for (int x = 0; x < tiles_len; x++) { for (int x = 0; x < tiles_len; x++) {
JSValue col = JS_GetPropertyUint32(js, tiles_array, x); JSValue col = JS_GetPropertyNumber(js, tiles_array, x);
if (JS_IsArray(js, col)) { if (JS_IsArray(col)) {
int col_len = JS_ArrayLength(js, col); int col_len = JS_ArrayLength(js, col);
for (int y = 0; y < col_len; y++) { 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)) { if (!JS_IsNull(tile) && !JS_IsNull(tile)) {
tile_count++; tile_count++;
} }
@@ -657,11 +657,11 @@ JSC_CCALL(geometry_tilemap_to_data,
int index_idx = 0; int index_idx = 0;
for (int x = 0; x < tiles_len; x++) { for (int x = 0; x < tiles_len; x++) {
JSValue col = JS_GetPropertyUint32(js, tiles_array, x); JSValue col = JS_GetPropertyNumber(js, tiles_array, x);
if (JS_IsArray(js, col)) { if (JS_IsArray(col)) {
int col_len = JS_ArrayLength(js, col); int col_len = JS_ArrayLength(js, col);
for (int y = 0; y < col_len; y++) { 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)) { if (!JS_IsNull(tile) && !JS_IsNull(tile)) {
// Calculate world position // Calculate world position
// x and y are array indices, need to convert to logical coordinates // 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, JSC_CCALL(geometry_sprites_to_data,
JSValue sprites_array = argv[0]; 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"); return JS_ThrowTypeError(js, "sprites must be an array");
} }
@@ -804,7 +804,7 @@ JSC_CCALL(geometry_sprites_to_data,
int index_idx = 0; int index_idx = 0;
for (int i = 0; i < sprite_count; i++) { 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 // Get sprite properties
JSValue pos_val = JS_GetPropertyStr(js, sprite, "pos"); JSValue pos_val = JS_GetPropertyStr(js, sprite, "pos");
@@ -989,16 +989,16 @@ JSC_CCALL(geometry_transform_xy_blob,
} }
JSValue camera_params = argv[1]; 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]"); return JS_ThrowTypeError(js, "Second argument must be an array of 4 camera transform parameters [a, c, e, f]");
} }
// Get camera transform parameters // Get camera transform parameters
double a, c, e, f; double a, c, e, f;
JSValue a_val = JS_GetPropertyUint32(js, camera_params, 0); JSValue a_val = JS_GetPropertyNumber(js, camera_params, 0);
JSValue c_val = JS_GetPropertyUint32(js, camera_params, 1); JSValue c_val = JS_GetPropertyNumber(js, camera_params, 1);
JSValue e_val = JS_GetPropertyUint32(js, camera_params, 2); JSValue e_val = JS_GetPropertyNumber(js, camera_params, 2);
JSValue f_val = JS_GetPropertyUint32(js, camera_params, 3); JSValue f_val = JS_GetPropertyNumber(js, camera_params, 3);
JS_ToFloat64(js, &a, a_val); JS_ToFloat64(js, &a, a_val);
JS_ToFloat64(js, &c, c_val); JS_ToFloat64(js, &c, c_val);
@@ -1058,7 +1058,7 @@ JSC_CCALL(geometry_array_blob,
size_t len = JS_ArrayLength(js,arr); size_t len = JS_ArrayLength(js,arr);
float data[len]; float data[len];
for (int i = 0; i < len; i++) { 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); data[i] = js2number(js, val);
JS_FreeValue(js,val); JS_FreeValue(js,val);
} }
@@ -1168,7 +1168,7 @@ JSC_CCALL(geometry_weave,
int num_elements = -1; int num_elements = -1;
for (uint32_t i = 0; i < stream_count; i++) { 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 data_blob = JS_GetPropertyStr(js, stream_obj, "data");
JSValue stride_val = JS_GetPropertyStr(js, stream_obj, "stride"); JSValue stride_val = JS_GetPropertyStr(js, stream_obj, "stride");

View File

@@ -17,11 +17,23 @@ gesture.reset = function() {
gesture.on_input = function(action_id, action) { gesture.on_input = function(action_id, action) {
if (search(action_id, 'gamepad_touchpad_') == null) return if (search(action_id, 'gamepad_touchpad_') == null) return
var finger = action.finger || 0 var finger = action.finger || 0
var touchpad = action.touchpad || 0 var touchpad = action.touchpad || 0
var fingerId = `${touchpad}_${finger}` 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') { if (action_id == 'gamepad_touchpad_down') {
// Add new touch // Add new touch
this.touches[fingerId] = { this.touches[fingerId] = {
@@ -31,9 +43,9 @@ gesture.on_input = function(action_id, action) {
startY: action.y, startY: action.y,
startTime: time.number() startTime: time.number()
} }
var touchCount = length(array(this.touches)) touchCount = length(array(this.touches))
if (touchCount == 1) { if (touchCount == 1) {
// Single touch started // Single touch started
this.gestureState = 'single' this.gestureState = 'single'
@@ -41,7 +53,7 @@ gesture.on_input = function(action_id, action) {
} else if (touchCount == 2) { } else if (touchCount == 2) {
// Two touches - potential pinch // Two touches - potential pinch
this.gestureState = 'multi' 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]) this.startDist = this.dist(fingers[0], fingers[1])
} }
} }
@@ -50,17 +62,17 @@ gesture.on_input = function(action_id, action) {
// Update touch position // Update touch position
this.touches[fingerId].x = action.x this.touches[fingerId].x = action.x
this.touches[fingerId].y = action.y this.touches[fingerId].y = action.y
var touchCount = length(array(this.touches)) touchCount = length(array(this.touches))
if (touchCount == 2 && this.gestureState == 'multi') { if (touchCount == 2 && this.gestureState == 'multi') {
// Check for pinch gesture // Check for pinch gesture
var fingers = array(array(this.touches), k => this.touches[k]) fingers = array(array(this.touches), k => this.touches[k])
var currentDist = this.dist(fingers[0], fingers[1]) currentDist = this.dist(fingers[0], fingers[1])
var d = currentDist - this.startDist d = currentDist - this.startDist
if (abs(d) >= this.PINCH_TH) { 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 }]) // scene.recurse(game.root, 'on_input', [gesture_type, { delta: d }])
this.startDist = currentDist this.startDist = currentDist
} }
@@ -69,19 +81,20 @@ gesture.on_input = function(action_id, action) {
} }
else if (action_id == 'gamepad_touchpad_up') { else if (action_id == 'gamepad_touchpad_up') {
if (this.touches[fingerId]) { if (this.touches[fingerId]) {
var touch = this.touches[fingerId] touch = this.touches[fingerId]
var touchCount = length(array(this.touches)) touchCount = length(array(this.touches))
// Check for swipe if this was the only/last touch // Check for swipe if this was the only/last touch
if (touchCount == 1 && this.gestureState == 'single') { if (touchCount == 1 && this.gestureState == 'single') {
var dt = time.number() - touch.startTime dt = time.number() - touch.startTime
var dx = action.x - touch.startX dx = action.x - touch.startX
var dy = action.y - touch.startY dy = action.y - touch.startY
if (dt < this.MAX_TIME / 1000) { // Convert to seconds 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 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') ? (dx > 0 ? 'swipe_right' : 'swipe_left')
: (dy > 0 ? 'swipe_down' : 'swipe_up') : (dy > 0 ? 'swipe_down' : 'swipe_up')
audio.play('swipe') audio.play('swipe')
@@ -89,10 +102,10 @@ gesture.on_input = function(action_id, action) {
} }
} }
} }
// Remove touch // Remove touch
delete this.touches[fingerId] delete this.touches[fingerId]
// Reset if no touches left // Reset if no touches left
if (length(array(this.touches)) == 0) { if (length(array(this.touches)) == 0) {
this.gestureState = null this.gestureState = null

View File

@@ -42,7 +42,7 @@ function calc_image_size(img) {
function decorate_rect_px(img) { function decorate_rect_px(img) {
// default UV rect is the whole image if none supplied // 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; var width = 0, height = 0;
if (img.texture) { if (img.texture) {
@@ -87,27 +87,32 @@ function wrapFrames(arr){ /* [{surface,time,rect}, …] → [{image,t
} }
}); });
} }
function makeAnim(frames, loop=true){ function makeAnim(frames, loop){
return { frames, loop } var local_loop = loop != null ? loop : true
return { frames: frames, loop: local_loop }
} }
function decode_image(bytes, ext) function decode_image(bytes, ext)
{ {
switch(ext) { var qoi_result = null
case 'gif': return decode_gif(gif.decode(bytes)) if (ext == 'gif') {
case 'ase': return decode_gif(gif.decode(bytes))
case 'aseprite': return decode_aseprite(aseprite.decode(bytes)) } else if (ext == 'ase' || ext == 'aseprite') {
case 'qoi': return qoi.decode(bytes) // returns single surface return decode_aseprite(aseprite.decode(bytes))
case 'png': return png.decode(bytes) // returns single surface } else if (ext == 'qoi') {
case 'jpg': return qoi.decode(bytes) // returns single surface
case 'jpeg': return png.decode(bytes) // png.decode handles jpg too via stb_image } else if (ext == 'png') {
case 'bmp': return png.decode(bytes) // png.decode handles bmp too via stb_image return png.decode(bytes) // returns single surface
default: } else if (ext == 'jpg' || ext == 'jpeg') {
// Try QOI first since it's fast to check return png.decode(bytes) // png.decode handles jpg too via stb_image
var qoi_result = qoi.decode(bytes) } else if (ext == 'bmp') {
if (qoi_result) return qoi_result return png.decode(bytes) // png.decode handles bmp too via stb_image
// Fall back to png decoder for other formats (uses stb_image) } else {
return png.decode(bytes) // 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){ function create_image(path){
try{ def bytes = io.slurp(path);
def bytes = io.slurp(path);
var ext = pop(array(path, '.')) var ext = pop(array(path, '.'))
var raw = decode_image(bytes, ext); var raw = decode_image(bytes, ext);
var anims = null
/* ── Case A: single surface (from make_texture) ────────────── */ var keys = null
if(raw && raw.width && raw.pixels && !is_array(raw)) {
return graphics.Image(raw)
}
/* ── Case B: array of surfaces (from make_gif) ────────────── */ /* ── Case A: single surface (from make_texture) ────────────── */
if(is_array(raw)) { if(raw && raw.width && raw.pixels && !is_array(raw)) {
// Single frame GIF returns array with one surface return graphics.Image(raw)
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 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 = {} var image = {}
@@ -212,7 +214,7 @@ image.dimensions = function() {
return [width, height].scale([this.rect[2], this.rect[3]]) return [width, height].scale([this.rect[2], this.rect[3]])
} }
var spritesheet var spritesheet = null
var sheet_frames = [] var sheet_frames = []
var sheetsize = 1024 var sheetsize = 1024
@@ -249,8 +251,10 @@ graphics.from_surface = function(surf)
graphics.from = function(id, data) graphics.from = function(id, data)
{ {
if (!is_text(id)) if (!is_text(id)) {
throw Error('Expected a string ID') log.error('Expected a string ID')
return null
}
if (is_blob(data)) if (is_blob(data))
return graphics.texture_from_data(data) return graphics.texture_from_data(data)
@@ -258,14 +262,20 @@ graphics.from = function(id, data)
graphics.texture = function texture(path) { graphics.texture = function texture(path) {
if (is_proto(path, graphics.Image)) return path if (is_proto(path, graphics.Image)) return path
if (!is_text(path)) if (!is_text(path)) {
throw Error('need a string for graphics.texture') log.error('need a string for graphics.texture')
return null
}
var parts = array(path, ':') var parts = array(path, ':')
var id = parts[0] var id = parts[0]
var animName = parts[1] var animName = parts[1]
var frameIndex = parts[2] 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") // Handle the case where animName is actually a frame index (e.g., "gears:0")
if (animName != null && frameIndex == null && !is_null(number(animName))) { if (animName != null && frameIndex == null && !is_null(number(animName))) {
@@ -274,14 +284,14 @@ graphics.texture = function texture(path) {
} }
if (!cache[id]) { if (!cache[id]) {
var ipath = res.find_image(id) ipath = res.find_image(id)
if (!ipath) { if (!ipath) {
// If still not found, return notex // If still not found, return notex
return graphics.texture('notex') return graphics.texture('notex')
} }
var result = create_image(ipath) result = create_image(ipath)
cache[id] = result cache[id] = result
} }
@@ -294,7 +304,7 @@ graphics.texture = function texture(path) {
if (!animName && frameIndex != null) { if (!animName && frameIndex != null) {
// If cached is a single animation (has .frames property) // If cached is a single animation (has .frames property)
if (cached.frames && is_array(cached.frames)) { if (cached.frames && is_array(cached.frames)) {
var idx = number(frameIndex) idx = number(frameIndex)
if (idx == null) return cached if (idx == null) return cached
// Wrap the index // Wrap the index
idx = idx % length(cached.frames) 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 is a single animation (has .frames property)
if (cached.frames && is_array(cached.frames)) { if (cached.frames && is_array(cached.frames)) {
if (frameIndex != null) { if (frameIndex != null) {
var idx = number(frameIndex) idx = number(frameIndex)
if (isNaN(idx)) return cached if (idx == null) return cached
// Wrap the index // Wrap the index
idx = idx % length(cached.frames) idx = idx % length(cached.frames)
return cached.frames[idx].image return cached.frames[idx].image
@@ -331,13 +341,14 @@ graphics.texture = function texture(path) {
// If cached is an object of multiple animations // If cached is an object of multiple animations
if (is_object(cached) && !cached.frames) { if (is_object(cached) && !cached.frames) {
var anim = cached[animName] anim = cached[animName]
if (!anim) if (!anim)
throw Error(`animation ${animName} not found in ${id}`) log.error(`animation ${animName} not found in ${id}`)
return null
if (frameIndex != null) { if (frameIndex != null) {
var idx = number(frameIndex) idx = number(frameIndex)
if (isNaN(idx)) return anim if (idx == null) return anim
if (is_proto(anim, graphics.Image)) { if (is_proto(anim, graphics.Image)) {
// Single image animation - any frame index returns the image // Single image animation - any frame index returns the image
@@ -412,19 +423,25 @@ var datas = []
graphics.get_font = function get_font(path) { graphics.get_font = function get_font(path) {
if (is_object(path)) return path if (is_object(path)) return path
if (!is_text(path)) if (!is_text(path)) {
throw Error(`Can't find font with path: ${path}`) log.error(`Can't find font with path: ${path}`)
return null
}
var parts = array(path, '.') var parts = array(path, '.')
var size = 16 // default size var size = 16 // default size
var font_path = path
parts[1] = number(parts[1]) parts[1] = number(parts[1])
if (parts[1]) { if (parts[1]) {
path = parts[0] font_path = parts[0]
size = parts[1] size = parts[1]
} }
var fullpath = res.find_font(path) var fullpath = res.find_font(font_path)
if (!fullpath) throw Error(`Cannot load font ${path}`) if (!fullpath) {
log.error(`Cannot load font ${path}`)
return null
}
var fontstr = `${fullpath}.${size}` var fontstr = `${fullpath}.${size}`
if (fontcache[fontstr]) return fontcache[fontstr] if (fontcache[fontstr]) return fontcache[fontstr]
@@ -441,7 +458,8 @@ graphics.queue_sprite_mesh = function(queue) {
var sprites = filter(queue, x => x.type == 'sprite') var sprites = filter(queue, x => x.type == 'sprite')
if (length(sprites) == 0) return [] if (length(sprites) == 0) return []
var mesh = graphics.make_sprite_mesh(sprites) 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].mesh = mesh
sprites[i].first_index = i*6 sprites[i].first_index = i*6
sprites[i].num_indices = 6 sprites[i].num_indices = 6

View File

@@ -38,8 +38,8 @@ static inline ImVec2 js2imvec2(JSContext *js, JSValue v)
{ {
ImVec2 c; ImVec2 c;
double dx, dy; double dx, dy;
JSValue x = JS_GetPropertyUint32(js, v, 0); JSValue x = JS_GetPropertyNumber(js, v, 0);
JSValue y = JS_GetPropertyUint32(js, v, 1); JSValue y = JS_GetPropertyNumber(js, v, 1);
JS_ToFloat64(js, &dx, x); JS_ToFloat64(js, &dx, x);
JS_ToFloat64(js, &dy, y); JS_ToFloat64(js, &dy, y);
JS_FreeValue(js, x); JS_FreeValue(js, x);
@@ -53,10 +53,10 @@ static inline ImVec4 js2imvec4(JSContext *js, JSValue v)
{ {
ImVec4 c; ImVec4 c;
double dx, dy, dz, dw; double dx, dy, dz, dw;
JSValue x = JS_GetPropertyUint32(js, v, 0); JSValue x = JS_GetPropertyNumber(js, v, 0);
JSValue y = JS_GetPropertyUint32(js, v, 1); JSValue y = JS_GetPropertyNumber(js, v, 1);
JSValue z = JS_GetPropertyUint32(js, v, 2); JSValue z = JS_GetPropertyNumber(js, v, 2);
JSValue w = JS_GetPropertyUint32(js, v, 3); JSValue w = JS_GetPropertyNumber(js, v, 3);
JS_ToFloat64(js, &dx, x); JS_ToFloat64(js, &dx, x);
JS_ToFloat64(js, &dy, y); JS_ToFloat64(js, &dy, y);
JS_ToFloat64(js, &dz, z); 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) static inline JSValue imvec22js(JSContext *js, ImVec2 vec)
{ {
JSValue v = JS_NewObject(js); JSValue v = JS_NewObject(js);
JS_SetPropertyUint32(js, v, 0, JS_NewFloat64(js, vec.x)); JS_SetPropertyNumber(js, v, 0, JS_NewFloat64(js, vec.x));
JS_SetPropertyUint32(js, v, 1, JS_NewFloat64(js, vec.y)); JS_SetPropertyNumber(js, v, 1, JS_NewFloat64(js, vec.y));
return v; return v;
} }
@@ -169,17 +169,17 @@ JSC_SCALL(imgui_combo,
if (JS_IsNumber(argv[1])) { if (JS_IsNumber(argv[1])) {
current_item = js2number(js, 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]); preview_str = JS_ToCString(js, argv[1]);
} }
if (JS_IsArray(js, argv[2])) { if (JS_IsArray(argv[2])) {
// Handle array of strings // Handle array of strings
int item_count = JS_ArrayLength(js, argv[2]); int item_count = JS_ArrayLength(js, argv[2]);
const char **items = (const char**)malloc(sizeof(char*) * item_count); const char **items = (const char**)malloc(sizeof(char*) * item_count);
for (int i = 0; i < item_count; i++) { 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); items[i] = JS_ToCString(js, item);
JS_FreeValue(js, item); JS_FreeValue(js, item);
} }
@@ -210,7 +210,7 @@ JSC_SCALL(imgui_combo,
} }
free(items); free(items);
} else if (JS_IsString(argv[2])) { } else if (JS_IsText(argv[2])) {
// Handle single string with \0 separators // Handle single string with \0 separators
const char *items_str = JS_ToCString(js, argv[2]); 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 low = JS_IsNull(argv[2]) ? 0.0 : js2number(js, argv[2]);
float high = JS_IsNull(argv[3]) ? 1.0 : js2number(js, argv[3]); 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]); int n = JS_ArrayLength(js, argv[1]);
float a[4]; // Max 4 elements for SliderFloat4 float a[4]; // Max 4 elements for SliderFloat4
// Read values from JS array // Read values from JS array
for (int i = 0; i < n && i < 4; i++) { 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; double d;
JS_ToFloat64(js, &d, val); JS_ToFloat64(js, &d, val);
a[i] = (float)d; a[i] = (float)d;
@@ -276,7 +276,7 @@ JSC_SCALL(imgui_slider,
// Write values back to JS array // Write values back to JS array
ret = JS_NewArray(js); ret = JS_NewArray(js);
for (int i = 0; i < n && i < 4; i++) { 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 { } else {
float val = js2number(js, argv[1]); 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 low = JS_IsNull(argv[2]) ? 0 : js2number(js, argv[2]);
int high = JS_IsNull(argv[3]) ? 100 : js2number(js, argv[3]); 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 n = JS_ArrayLength(js, argv[1]);
int a[4]; // Max 4 elements for SliderInt4 int a[4]; // Max 4 elements for SliderInt4
// Read values from JS array // Read values from JS array
for (int i = 0; i < n && i < 4; i++) { 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; double d;
JS_ToFloat64(js, &d, val); JS_ToFloat64(js, &d, val);
a[i] = (int)d; a[i] = (int)d;
@@ -317,7 +317,7 @@ JSC_SCALL(imgui_intslider,
// Write values back to JS array // Write values back to JS array
ret = JS_NewArray(js); ret = JS_NewArray(js);
for (int i = 0; i < n && i < 4; i++) { 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 { } else {
int val = js2number(js, argv[1]); int val = js2number(js, argv[1]);

View File

@@ -147,69 +147,73 @@ function create_user(index, config) {
// Pick user based on pairing policy // Pick user based on pairing policy
function pick_user(canon) { function pick_user(canon) {
var picked = null
var old_down = null
var i = 0
if (length(_users) == 0) return null if (length(_users) == 0) return null
// For last_used: always user 0, just update active device // For last_used: always user 0, just update active device
if (_config.pairing == 'last_used') { if (_config.pairing == 'last_used') {
var user = _users[0] picked = _users[0]
// Only switch on button press, not axis/motion // Only switch on button press, not axis/motion
if (canon.kind == 'button' && canon.pressed) { 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 // Release all held actions when switching device
var old_down = user.router.down old_down = picked.router.down
arrfor(array(old_down), action => { arrfor(array(old_down), action => {
if (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 picked.active_device = canon.device_id
if (find(user.paired_devices, canon.device_id) == null) { if (find(picked.paired_devices, canon.device_id) == null) {
push(user.paired_devices, canon.device_id) push(picked.paired_devices, canon.device_id)
} }
} }
} }
return user return picked
} }
// For explicit pairing: find user paired to this device // 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) { if (find(_users[i].paired_devices, canon.device_id) != null) {
_users[i].active_device = canon.device_id _users[i].active_device = canon.device_id
return _users[i] return _users[i]
} }
} }
// Unpaired device - could implement join logic here // Unpaired device - could implement join logic here
return null return null
} }
// Configure the input system // Configure the input system
function configure(opts) { function configure(o) {
opts = opts || {} var opts = o || {}
var i = 0
_config.max_users = opts.max_users || 1 _config.max_users = opts.max_users || 1
_config.pairing = opts.pairing || 'last_used' _config.pairing = opts.pairing || 'last_used'
_config.emacs = opts.emacs != false _config.emacs = opts.emacs != false
_config.gestures = opts.gestures != false _config.gestures = opts.gestures != false
if (opts.action_map) _config.action_map = opts.action_map if (opts.action_map) _config.action_map = opts.action_map
if (opts.display_names) _config.display_names = opts.display_names if (opts.display_names) _config.display_names = opts.display_names
if (opts.on_window) _window_callback = opts.on_window if (opts.on_window) _window_callback = opts.on_window
// Copy gesture config // Copy gesture config
_config.swipe_min_dist = opts.swipe_min_dist _config.swipe_min_dist = opts.swipe_min_dist
_config.swipe_max_time = opts.swipe_max_time _config.swipe_max_time = opts.swipe_max_time
_config.pinch_threshold = opts.pinch_threshold _config.pinch_threshold = opts.pinch_threshold
// Create users // Create users
_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)) push(_users, create_user(i, _config))
} }
_initialized = true _initialized = true
} }

View File

@@ -22,13 +22,15 @@ function parse_mods(mod) {
// Translate SDL event to canonical format // Translate SDL event to canonical format
// Returns canonical event or null if not translatable // Returns canonical event or null if not translatable
function translate(evt) { function translate(evt) {
var control = null
// Keyboard // Keyboard
if (evt.type == 'key_down' || evt.type == 'key_up') { if (evt.type == 'key_down' || evt.type == 'key_up') {
if (evt.repeat) return null // Ignore key repeats 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 if (!control) return null
return { return {
kind: 'button', kind: 'button',
device_id: 'kbm', device_id: 'kbm',

View File

@@ -17,30 +17,30 @@ var controller_map = {
joyconpair: 'joyconpair' joyconpair: 'joyconpair'
} }
function make(defaults, display_names) { function make(defs, names) {
defaults = defaults || {} var d = defs || {}
display_names = display_names || {} var n = names || {}
var bindings = { var bindings = {
action_map: {}, action_map: {},
_defaults: {}, _defaults: {},
display_names: display_names, display_names: n,
is_rebinding: false, is_rebinding: false,
rebind_target: null rebind_target: null
} }
// Copy defaults // Copy defaults
arrfor(array(defaults), function(key){ arrfor(array(d), function(key){
var val = defaults[key] var val = d[key]
bindings.action_map[key] = is_array(val) ? array(val) : [val] bindings.action_map[key] = is_array(val) ? array(val) : [val]
bindings._defaults[key] = array(bindings.action_map[key]) bindings._defaults[key] = array(bindings.action_map[key])
}) })
// Get actions that match a control // Get actions that match a control
bindings.get_actions = function(control) { bindings.get_actions = function(control) {
return filter(array(this.action_map), action => find(this.action_map[action], control) != null) return filter(array(this.action_map), action => find(this.action_map[action], control) != null)
} }
// Get bindings for a specific device kind // Get bindings for a specific device kind
bindings.get_bindings_for_device = function(action, device_kind) { bindings.get_bindings_for_device = function(action, device_kind) {
var all = this.action_map[action] || [] var all = this.action_map[action] || []
@@ -57,69 +57,76 @@ function make(defaults, display_names) {
return true return true
}) })
} }
// Get primary binding for display // Get primary binding for display
bindings.get_primary_binding = function(action, device_kind) { bindings.get_primary_binding = function(action, device_kind) {
var device_bindings = this.get_bindings_for_device(action, device_kind) var device_bindings = this.get_bindings_for_device(action, device_kind)
if (length(device_bindings)) return device_bindings[0] if (length(device_bindings)) return device_bindings[0]
var all = this.action_map[action] || [] var all = this.action_map[action] || []
return length(all) > 0 ? all[0] : null return length(all) > 0 ? all[0] : null
} }
// Get icon for action // Get icon for action
bindings.get_icon_for_action = function(action, device_kind, gamepad_type) { bindings.get_icon_for_action = function(action, device_kind, gamepad_type) {
var binding = this.get_primary_binding(action, device_kind) 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 (!binding) return null
if (device_kind == 'keyboard') { if (device_kind == 'keyboard') {
if (starts_with(binding, 'mouse_button_')) { if (starts_with(binding, 'mouse_button_')) {
var button = replace(binding, 'mouse_button_', '') button = replace(binding, 'mouse_button_', '')
return 'ui/mouse/mouse_' + button + '.png' return 'ui/mouse/mouse_' + button + '.png'
} else { } else {
var key_map = { key_map = {
'escape': 'escape', 'return': 'return', 'space': 'space', 'escape': 'escape', 'return': 'return', 'space': 'space',
'up': 'arrow_up', 'down': 'arrow_down', 'left': 'arrow_left', 'right': 'arrow_right' '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' return 'ui/keyboard/keyboard_' + key + '.png'
} }
} }
if (device_kind == 'gamepad' && gamepad_type) { if (device_kind == 'gamepad' && gamepad_type) {
var prefix = controller_map[gamepad_type] || 'playstation' gp_prefix = controller_map[gamepad_type] || 'playstation'
var is_ps = find(['ps3', 'ps4', 'ps5'], gamepad_type) != null is_ps = find(['ps3', 'ps4', 'ps5'], gamepad_type) != null
var gamepad_map = { gamepad_map = {
'gamepad_a': is_ps ? 'playstation_button_cross' : 'xbox_button_a', 'gamepad_a': is_ps ? 'playstation_button_cross' : 'xbox_button_a',
'gamepad_b': is_ps ? 'playstation_button_circle' : 'xbox_button_b', 'gamepad_b': is_ps ? 'playstation_button_circle' : 'xbox_button_b',
'gamepad_x': is_ps ? 'playstation_button_square' : 'xbox_button_x', 'gamepad_x': is_ps ? 'playstation_button_square' : 'xbox_button_x',
'gamepad_y': is_ps ? 'playstation_button_triangle' : 'xbox_button_y', 'gamepad_y': is_ps ? 'playstation_button_triangle' : 'xbox_button_y',
'gamepad_dpup': prefix + '_dpad_up', 'gamepad_dpup': gp_prefix + '_dpad_up',
'gamepad_dpdown': prefix + '_dpad_down', 'gamepad_dpdown': gp_prefix + '_dpad_down',
'gamepad_dpleft': prefix + '_dpad_left', 'gamepad_dpleft': gp_prefix + '_dpad_left',
'gamepad_dpright': prefix + '_dpad_right', 'gamepad_dpright': gp_prefix + '_dpad_right',
'gamepad_l1': prefix + '_trigger_l1', 'gamepad_l1': gp_prefix + '_trigger_l1',
'gamepad_r1': prefix + '_trigger_r1', 'gamepad_r1': gp_prefix + '_trigger_r1',
'gamepad_l2': prefix + '_trigger_l2', 'gamepad_l2': gp_prefix + '_trigger_l2',
'gamepad_r2': prefix + '_trigger_r2', 'gamepad_r2': gp_prefix + '_trigger_r2',
'gamepad_start': this._get_start_icon(gamepad_type) 'gamepad_start': this._get_start_icon(gamepad_type)
} }
var icon = gamepad_map[binding] icon = gamepad_map[binding]
if (icon) return 'ui/' + prefix + '/' + icon + '.png' if (icon) return 'ui/' + gp_prefix + '/' + icon + '.png'
} }
return null return null
} }
bindings._get_start_icon = function(gamepad_type) { bindings._get_start_icon = function(gamepad_type) {
if (gamepad_type == 'ps3') return 'playstation3_button_start' if (gamepad_type == 'ps3') return 'playstation3_button_start'
if (gamepad_type == 'ps4') return 'playstation4_button_options' if (gamepad_type == 'ps4') return 'playstation4_button_options'
if (gamepad_type == 'ps5') return 'playstation5_button_options' if (gamepad_type == 'ps5') return 'playstation5_button_options'
return 'xbox_button_start' return 'xbox_button_start'
} }
// Start rebinding // Start rebinding
bindings.start_rebind = function(action) { bindings.start_rebind = function(action) {
if (!this.action_map[action]) return false if (!this.action_map[action]) return false
@@ -127,73 +134,75 @@ function make(defaults, display_names) {
this.rebind_target = action this.rebind_target = action
return true return true
} }
// Complete rebinding with new control // Complete rebinding with new control
bindings.rebind = function(action, new_control, device_kind) { 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 if (!this.action_map[action]) return false
// Remove from other actions // Remove from other actions
arrfor(array(this.action_map), act => { arrfor(array(this.action_map), act => {
var idx = search(this.action_map[act], new_control) var idx = search(this.action_map[act], new_control)
if (idx >= 0) if (idx >= 0)
this.action_map[act] = array(array(this.action_map[act], 0, idx), array(this.action_map[act], idx+1)) 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 // Clear existing bindings for this device kind
var target = this.action_map[action] target = this.action_map[action]
for (var i = length(target) - 1; i >= 0; i--) { for (i = length(target) - 1; i >= 0; i--) {
var existing_kind = starts_with(target[i], 'gamepad_') ? 'gamepad' : existing_kind = starts_with(target[i], 'gamepad_') ? 'gamepad' :
starts_with(target[i], 'swipe_') ? 'touch' : 'keyboard' starts_with(target[i], 'swipe_') ? 'touch' : 'keyboard'
if (existing_kind == device_kind) if (existing_kind == device_kind)
this.action_map[action] = array(array(this.action_map[action], 0, i), array(this.action_map[action], i+1)) this.action_map[action] = array(array(this.action_map[action], 0, i), array(this.action_map[action], i+1))
} }
// Add new binding // Add new binding
this.action_map[action].unshift(new_control) this.action_map[action].unshift(new_control)
this.is_rebinding = false this.is_rebinding = false
this.rebind_target = null this.rebind_target = null
return true return true
} }
// Cancel rebinding // Cancel rebinding
bindings.cancel_rebind = function() { bindings.cancel_rebind = function() {
this.is_rebinding = false this.is_rebinding = false
this.rebind_target = null this.rebind_target = null
} }
// Save bindings // Save bindings
bindings.save = function(path) { bindings.save = function(p) {
path = path || 'keybindings.json' var save_path = p || 'keybindings.json'
try { io.slurpwrite(save_path, json.encode(this.action_map))
io.slurpwrite(path, json.encode(this.action_map)) return true
return true } disruption {
} 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) {}
return false 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 // Reset to defaults
bindings.reset = function() { bindings.reset = function() {
arrfor(array(this._defaults), key => { arrfor(array(this._defaults), key => {
this.action_map[key] = array(this._defaults[key]) this.action_map[key] = array(this._defaults[key])
}) })
} }
return bindings return bindings
} }

View File

@@ -6,12 +6,13 @@ var _devices = {}
// Register a device from a canonical event // Register a device from a canonical event
function register(canon) { function register(canon) {
if (!canon || !canon.device_id) return if (!canon || !canon.device_id) return
var kind = null
if (!_devices[canon.device_id]) { if (!_devices[canon.device_id]) {
var kind = 'keyboard' kind = 'keyboard'
if (starts_with(canon.device_id, 'gp:')) kind = 'gamepad' if (starts_with(canon.device_id, 'gp:')) kind = 'gamepad'
else if (starts_with(canon.device_id, 'touch:')) kind = 'touch' else if (starts_with(canon.device_id, 'touch:')) kind = 'touch'
_devices[canon.device_id] = { _devices[canon.device_id] = {
id: canon.device_id, id: canon.device_id,
kind: kind, kind: kind,

View File

@@ -15,62 +15,76 @@ var emacs_special = {
} }
// Gesture stage - detects swipes and pinches from touchpad // Gesture stage - detects swipes and pinches from touchpad
function gesture_stage(config) { function gesture_stage(cfg) {
config = config || {} var o = cfg || {}
var min_swipe = config.swipe_min_dist || 30 var min_swipe = o.swipe_min_dist || 30
var max_time = config.swipe_max_time || 500 var max_time = o.swipe_max_time || 500
var pinch_th = config.pinch_threshold || 10 var pinch_th = o.pinch_threshold || 10
var touches = {} var touches = {}
var gesture_state = null var gesture_state = null
var start_dist = 0 var start_dist = 0
function dist(p1, p2) { function dist(p1, p2) {
var dx = p2[0] - p1[0] var dx = p2[0] - p1[0]
var dy = p2[1] - p1[1] var dy = p2[1] - p1[1]
return Math.sqrt(dx * dx + dy * dy) return Math.sqrt(dx * dx + dy * dy)
} }
return { return {
process: function(events) { process: function(events) {
var output = [] var output = []
var i = 0
for (var i = 0; i < length(events); i++) { var ev = null
var ev = events[i] 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 // Only process gamepad touchpad events
if (ev.control != 'gamepad_touchpad') { if (ev.control != 'gamepad_touchpad') {
push(output, ev) push(output, ev)
continue continue
} }
var fingerId = (ev.touchpad || 0) + '_' + (ev.finger || 0) fingerId = (ev.touchpad || 0) + '_' + (ev.finger || 0)
if (ev.pressed) { if (ev.pressed) {
touches[fingerId] = { touches[fingerId] = {
pos: ev.pos, pos: ev.pos,
startPos: ev.pos, startPos: ev.pos,
startTime: time.number() startTime: time.number()
} }
var count = length(array(touches)) count = length(array(touches))
if (count == 1) gesture_state = 'single' if (count == 1) gesture_state = 'single'
else if (count == 2) { else if (count == 2) {
gesture_state = 'multi' gesture_state = 'multi'
var fingers = Object.values(touches) fingers = Object.values(touches)
start_dist = dist(fingers[0].pos, fingers[1].pos) start_dist = dist(fingers[0].pos, fingers[1].pos)
} }
} }
else if (ev.kind == 'axis') { else if (ev.kind == 'axis') {
if (touches[fingerId]) { if (touches[fingerId]) {
touches[fingerId].pos = ev.pos touches[fingerId].pos = ev.pos
var count = length(array(touches)) count = length(array(touches))
if (count == 2 && gesture_state == 'multi') { if (count == 2 && gesture_state == 'multi') {
var fingers = Object.values(touches) fingers = Object.values(touches)
var currentDist = dist(fingers[0].pos, fingers[1].pos) currentDist = dist(fingers[0].pos, fingers[1].pos)
var d = currentDist - start_dist d = currentDist - start_dist
if (Math.abs(d) >= pinch_th / 100) { if (Math.abs(d) >= pinch_th / 100) {
push(output, { push(output, {
kind: 'gesture', kind: 'gesture',
@@ -88,21 +102,22 @@ function gesture_stage(config) {
} }
else if (ev.released) { else if (ev.released) {
if (touches[fingerId]) { if (touches[fingerId]) {
var touch = touches[fingerId] touch = touches[fingerId]
var count = length(array(touches)) count = length(array(touches))
if (count == 1 && gesture_state == 'single') { if (count == 1 && gesture_state == 'single') {
var dt = (time.number() - touch.startTime) * 1000 dt = (time.number() - touch.startTime) * 1000
var dx = ev.pos[0] - touch.startPos[0] dx = ev.pos[0] - touch.startPos[0]
var dy = ev.pos[1] - touch.startPos[1] dy = ev.pos[1] - touch.startPos[1]
if (dt < max_time) { 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) { if (absX > min_swipe / 100 || absY > min_swipe / 100) {
var dir = absX > absY dir = absX > absY
? (dx > 0 ? 'swipe_right' : 'swipe_left') ? (dx > 0 ? 'swipe_right' : 'swipe_left')
: (dy > 0 ? 'swipe_down' : 'swipe_up') : (dy > 0 ? 'swipe_down' : 'swipe_up')
push(output, { push(output, {
kind: 'gesture', kind: 'gesture',
device_id: ev.device_id, device_id: ev.device_id,
@@ -114,13 +129,13 @@ function gesture_stage(config) {
} }
} }
} }
delete touches[fingerId] delete touches[fingerId]
if (length(array(touches)) == 0) gesture_state = null if (length(array(touches)) == 0) gesture_state = null
} }
} }
} }
return output return output
} }
} }
@@ -129,50 +144,54 @@ function gesture_stage(config) {
// Emacs stage - converts keyboard input to emacs chords // Emacs stage - converts keyboard input to emacs chords
function emacs_stage() { function emacs_stage() {
var prefix = null var prefix = null
return { return {
process: function(events) { process: function(events) {
var output = [] var output = []
var i = 0
for (var i = 0; i < length(events); i++) { var ev = null
var ev = events[i] var notation = null
var chord = null
for (i = 0; i < length(events); i++) {
ev = events[i]
// Only process keyboard button events // Only process keyboard button events
if (ev.device_id != 'kbm' || ev.kind != 'button' || !ev.pressed) { if (ev.device_id != 'kbm' || ev.kind != 'button' || !ev.pressed) {
push(output, ev) push(output, ev)
continue continue
} }
/* if (find(valid_emacs_keys, ev.control) == null) { /* if (find(valid_emacs_keys, ev.control) == null) {
push(output, ev) push(output, ev)
continue continue
} }
*/ */
// Only process if we have modifiers OR waiting for chord // Only process if we have modifiers OR waiting for chord
if (!ev.mods?.ctrl && !ev.mods?.alt && !prefix) { if (!ev.mods?.ctrl && !ev.mods?.alt && !prefix) {
push(output, ev) push(output, ev)
continue continue
} }
var notation = "" notation = ""
if (ev.mods?.ctrl) notation += "C-" if (ev.mods?.ctrl) notation += "C-"
if (ev.mods?.alt) notation += "M-" if (ev.mods?.alt) notation += "M-"
if (length(ev.control) == 1) { if (length(ev.control) == 1) {
notation += lower(ev.control) notation += lower(ev.control)
} else { } else {
notation += emacs_special[ev.control] || ev.control notation += emacs_special[ev.control] || ev.control
} }
// Handle prefix keys // Handle prefix keys
if (notation == "C-x" || notation == "C-c") { if (notation == "C-x" || notation == "C-c") {
prefix = notation prefix = notation
continue // Consume, don't output continue // Consume, don't output
} }
// Complete chord if we have prefix // Complete chord if we have prefix
if (prefix) { if (prefix) {
var chord = prefix + " " + notation chord = prefix + " " + notation
prefix = null prefix = null
push(output, { push(output, {
kind: 'chord', kind: 'chord',
@@ -193,7 +212,7 @@ function emacs_stage() {
}) })
} }
} }
return output return output
} }
} }
@@ -202,34 +221,39 @@ function emacs_stage() {
// Action mapping stage - converts controls to named actions // Action mapping stage - converts controls to named actions
function action_stage(bindings) { function action_stage(bindings) {
var down = {} var down = {}
return { return {
down: down, down: down,
process: function(events) { process: function(events) {
var output = [] var output = []
var i = 0
for (var i = 0; i < length(events); i++) { var ev = null
var ev = events[i] var actions = null
var j = 0
var action = null
for (i = 0; i < length(events); i++) {
ev = events[i]
// Pass through non-button events // Pass through non-button events
if (ev.kind != 'button' && ev.kind != 'chord' && ev.kind != 'gesture') { if (ev.kind != 'button' && ev.kind != 'chord' && ev.kind != 'gesture') {
push(output, ev) push(output, ev)
continue continue
} }
var actions = bindings.get_actions(ev.control) actions = bindings.get_actions(ev.control)
if (length(actions) == 0) { if (length(actions) == 0) {
push(output, ev) push(output, ev)
continue continue
} }
for (var j = 0; j < length(actions); j++) { for (j = 0; j < length(actions); j++) {
var action = actions[j] action = actions[j]
if (ev.pressed) down[action] = true if (ev.pressed) down[action] = true
else if (ev.released) down[action] = false else if (ev.released) down[action] = false
push(output, { push(output, {
kind: 'action', kind: 'action',
device_id: ev.device_id, device_id: ev.device_id,
@@ -241,7 +265,7 @@ function action_stage(bindings) {
}) })
} }
} }
return output return output
} }
} }
@@ -251,55 +275,59 @@ function action_stage(bindings) {
function delivery_stage(user) { function delivery_stage(user) {
return { return {
process: function(events) { process: function(events) {
for (var i = 0; i < length(events); i++) { var i = 0
var ev = events[i] var ev = null
for (i = 0; i < length(events); i++) {
ev = events[i]
// Only deliver actions // Only deliver actions
if (ev.kind != 'action') continue if (ev.kind != 'action') continue
user.dispatch(ev.control, { user.dispatch(ev.control, {
pressed: ev.pressed, pressed: ev.pressed,
released: ev.released, released: ev.released,
time: ev.time time: ev.time
}) })
} }
return events return events
} }
} }
} }
// Create router with pipeline // Create router with pipeline
function make(user, config) { function make(user, cfg) {
config = config || {} var o = cfg || {}
var stages = [] var stages = []
var action = null var action = null
if (config.gestures != false) { if (o.gestures != false) {
push(stages, gesture_stage(config)) push(stages, gesture_stage(o))
} }
if (config.emacs != false) { if (o.emacs != false) {
push(stages, emacs_stage()) push(stages, emacs_stage())
} }
action = action_stage(user.bindings) action = action_stage(user.bindings)
push(stages, action) push(stages, action)
push(stages, delivery_stage(user)) push(stages, delivery_stage(user))
return { return {
stages: stages, stages: stages,
down() { return action.down }, down() { return action.down },
handle: function(canon) { handle: function(canon) {
var events = [canon] var events = [canon]
var i = 0
for (var i = 0; i < length(this.stages); i++) {
for (i = 0; i < length(this.stages); i++) {
events = this.stages[i].process(events) events = this.stages[i].process(events)
} }
return events return events
} }
} }

View File

@@ -39,9 +39,9 @@ static JSValue js_layout_set_size(JSContext *js, JSValueConst self, int argc, JS
double width = 0, height = 0; double width = 0, height = 0;
// Check if it's an array (for backwards compatibility) // Check if it's an array (for backwards compatibility)
if (JS_IsArray(js, argv[1])) { if (JS_IsArray(argv[1])) {
JSValue width_val = JS_GetPropertyUint32(js, argv[1], 0); JSValue width_val = JS_GetPropertyNumber(js, argv[1], 0);
JSValue height_val = JS_GetPropertyUint32(js, argv[1], 1); JSValue height_val = JS_GetPropertyNumber(js, argv[1], 1);
JS_ToFloat64(js, &width, width_val); JS_ToFloat64(js, &width, width_val);
JS_ToFloat64(js, &height, height_val); JS_ToFloat64(js, &height, height_val);
JS_FreeValue(js, width_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) { static void js_layout_finalizer(JSRuntime *rt, JSValue val) {
lay_context *ctx = JS_GetOpaque(val, js_layout_class_id); lay_context *ctx = JS_GetOpaque(val, js_layout_class_id);
lay_destroy_context(ctx); lay_destroy_context(ctx);
js_free_rt(rt, ctx); js_free_rt(ctx);
} }
static JSClassDef js_layout_class = { static JSClassDef js_layout_class = {
@@ -190,7 +190,7 @@ static const JSCFunctionListEntry js_layout_funcs[] = {
CELL_USE_INIT( CELL_USE_INIT(
JS_NewClassID(&js_layout_class_id); 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); JSValue proto = JS_NewObject(js);
JS_SetPropertyFunctionList(js, proto, js_layout_proto_funcs, sizeof(js_layout_proto_funcs) / sizeof(JSCFunctionListEntry)); JS_SetPropertyFunctionList(js, proto, js_layout_proto_funcs, sizeof(js_layout_proto_funcs) / sizeof(JSCFunctionListEntry));

225
line2d.cm
View File

@@ -78,117 +78,147 @@ function build_polyline_mesh(line) {
// Transform points if in local space // Transform points if in local space
var pts = [] var pts = []
for (var i = 0; i < length(points); i++) { var i = 0
var p = points[i] var p = null
var dx = 0
var dy = 0
for (i = 0; i < length(points); i++) {
p = points[i]
if (points_space == 'local') { if (points_space == 'local') {
push(pts, {x: p.x + pos.x, y: p.y + pos.y}) push(pts, {x: p.x + pos.x, y: p.y + pos.y})
} else { } else {
push(pts, {x: p.x, y: p.y}) push(pts, {x: p.x, y: p.y})
} }
} }
// Calculate cumulative distances // Calculate cumulative distances
var cumulative = [0] var cumulative = [0]
for (var i = 1; i < length(pts); i++) { for (i = 1; i < length(pts); i++) {
var dx = pts[i].x - pts[i-1].x dx = pts[i].x - pts[i-1].x
var dy = pts[i].y - pts[i-1].y dy = pts[i].y - pts[i-1].y
push(cumulative, cumulative[i-1] + math.sqrt(dx*dx + dy*dy)) push(cumulative, cumulative[i-1] + math.sqrt(dx*dx + dy*dy))
} }
var total_length = cumulative[length(cumulative) - 1] var total_length = cumulative[length(cumulative) - 1]
// Build triangle strip mesh // Build triangle strip mesh
var verts = [] var verts = []
var indices = [] var indices = []
// Get width at point i // Get width at point i
function get_width(i) { function get_width(idx) {
if (widths && length(widths) > i) return widths[i] if (widths && length(widths) > idx) return widths[idx]
return width return width
} }
// Get U coordinate at point i // 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') { 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') { } else if (uv_mode == 'per_segment') {
if (i == 0) return 0 if (idx == 0) return 0
var seg_idx = i - 1 seg_idx = idx - 1
var seg_len = cumulative[i] - cumulative[i-1] seg_len = cumulative[idx] - cumulative[idx-1]
return 1 // Each segment ends at u=1 return 1 // Each segment ends at u=1
} else { } else {
// repeat (default) // repeat (default)
return cumulative[i] * u_per_unit + u_offset return cumulative[idx] * u_per_unit + u_offset
} }
} }
// Calculate normals at each point // Calculate normals at each point
var normals = [] var normals = []
for (var i = 0; i < length(pts); i++) { var prev = null
var prev = i > 0 ? pts[i-1] : (closed ? pts[length(pts)-1] : null) var curr = null
var curr = pts[i] var next = null
var next = i < length(pts)-1 ? pts[i+1] : (closed ? pts[0] : null) var n = null
var d1x = 0
var n = {x: 0, y: 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) { if (prev && next) {
// Middle point - average normals // Middle point - average normals
var d1x = curr.x - prev.x d1x = curr.x - prev.x
var d1y = curr.y - prev.y d1y = curr.y - prev.y
var d2x = next.x - curr.x d2x = next.x - curr.x
var d2y = next.y - curr.y d2y = next.y - curr.y
var len1 = math.sqrt(d1x*d1x + d1y*d1y) len1 = math.sqrt(d1x*d1x + d1y*d1y)
var len2 = math.sqrt(d2x*d2x + d2y*d2y) len2 = math.sqrt(d2x*d2x + d2y*d2y)
if (len1 > 0.0001) { d1x /= len1; d1y /= len1 } if (len1 > 0.0001) { d1x /= len1; d1y /= len1 }
if (len2 > 0.0001) { d2x /= len2; d2y /= len2 } if (len2 > 0.0001) { d2x /= len2; d2y /= len2 }
// Normals (perpendicular) // Normals (perpendicular)
var n1x = -d1y, n1y = d1x n1x = -d1y
var n2x = -d2y, n2y = d2x n1y = d1x
n2x = -d2y
n2y = d2x
// Average // Average
n.x = n1x + n2x n.x = n1x + n2x
n.y = n1y + n2y 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 } if (nlen > 0.0001) { n.x /= nlen; n.y /= nlen }
// Miter correction // Miter correction
var dot = n1x * n.x + n1y * n.y dot = n1x * n.x + n1y * n.y
if (dot > 0.0001) { if (dot > 0.0001) {
var miter_scale = 1 / dot miter_scale = 1 / dot
if (miter_scale > miter_limit) miter_scale = miter_limit if (miter_scale > miter_limit) miter_scale = miter_limit
n.x *= miter_scale n.x *= miter_scale
n.y *= miter_scale n.y *= miter_scale
} }
} else if (next) { } else if (next) {
// Start point // Start point
var dx = next.x - curr.x dx = next.x - curr.x
var dy = next.y - curr.y dy = next.y - curr.y
var len = math.sqrt(dx*dx + dy*dy) len = math.sqrt(dx*dx + dy*dy)
if (len > 0.0001) { dx /= len; dy /= len } if (len > 0.0001) { dx /= len; dy /= len }
n.x = -dy n.x = -dy
n.y = dx n.y = dx
} else if (prev) { } else if (prev) {
// End point // End point
var dx = curr.x - prev.x dx = curr.x - prev.x
var dy = curr.y - prev.y dy = curr.y - prev.y
var len = math.sqrt(dx*dx + dy*dy) len = math.sqrt(dx*dx + dy*dy)
if (len > 0.0001) { dx /= len; dy /= len } if (len > 0.0001) { dx /= len; dy /= len }
n.x = -dy n.x = -dy
n.y = dx n.y = dx
} }
push(normals, n) push(normals, n)
} }
// Generate vertices (2 per point - left and right of line) // Generate vertices (2 per point - left and right of line)
for (var i = 0; i < length(pts); i++) { var w = 0
var p = pts[i] var u = 0
var n = normals[i] for (i = 0; i < length(pts); i++) {
var w = get_width(i) * 0.5 p = pts[i]
var u = get_u(i) n = normals[i]
w = get_width(i) * 0.5
u = get_u(i)
// Left vertex (v=0) // Left vertex (v=0)
push(verts, { push(verts, {
x: p.x + n.x * w, x: p.x + n.x * w,
@@ -197,7 +227,7 @@ function build_polyline_mesh(line) {
v: v_offset, v: v_offset,
r: 1, g: 1, b: 1, a: 1 r: 1, g: 1, b: 1, a: 1
}) })
// Right vertex (v=1) // Right vertex (v=1)
push(verts, { push(verts, {
x: p.x - n.x * w, x: p.x - n.x * w,
@@ -207,10 +237,11 @@ function build_polyline_mesh(line) {
r: 1, g: 1, b: 1, a: 1 r: 1, g: 1, b: 1, a: 1
}) })
} }
// Generate indices (triangle strip as triangles) // Generate indices (triangle strip as triangles)
for (var i = 0; i < length(pts) - 1; i++) { var base = 0
var base = i * 2 for (i = 0; i < length(pts) - 1; i++) {
base = i * 2
// First triangle // First triangle
push(indices, base + 0) push(indices, base + 0)
push(indices, base + 1) push(indices, base + 1)
@@ -220,10 +251,11 @@ function build_polyline_mesh(line) {
push(indices, base + 3) push(indices, base + 3)
push(indices, base + 2) push(indices, base + 2)
} }
// Handle closed path // Handle closed path
var last = 0
if (closed && length(pts) > 2) { if (closed && length(pts) > 2) {
var last = (length(pts) - 1) * 2 last = (length(pts) - 1) * 2
push(indices, last + 0) push(indices, last + 0)
push(indices, last + 1) push(indices, last + 1)
push(indices, 0) push(indices, 0)
@@ -234,11 +266,11 @@ function build_polyline_mesh(line) {
// Add round caps if requested // Add round caps if requested
if (!closed && cap == 'round') { 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: 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, 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[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') { } 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: 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, 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[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 { 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) { function add_round_cap(opts) {
var w = width * 0.5 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 segments = 8
var base_idx = length(verts) var base_idx = length(verts)
// Direction along the line // Direction along the line
var dx = is_start ? -n.y : n.y var dx = is_start ? -n.y : n.y
var dy = is_start ? n.x : -n.x var dy = is_start ? n.x : -n.x
// Center vertex // Center vertex
push(verts, { push(verts, {
x: p.x, 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, v: 0.5 * v_scale + v_offset,
r: 1, g: 1, b: 1, a: 1 r: 1, g: 1, b: 1, a: 1
}) })
// Arc vertices // Arc vertices
var start_angle = is_start ? math.arc_tangent(n.y, n.x) : math.arc_tangent(-n.y, -n.x) 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 i = 0
var angle = start_angle + (i / segments) * 3.14159 var angle = 0
var cx = math.cosine(angle) var cx = 0
var cy = math.sine(angle) 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, { push(verts, {
x: p.x + cx * w, x: p.x + cx * w,
y: p.y + cy * 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 r: 1, g: 1, b: 1, a: 1
}) })
} }
// Fan triangles // Fan triangles
for (var i = 0; i < segments; i++) { for (i = 0; i < segments; i++) {
push(indices, base_idx) push(indices, base_idx)
push(indices, base_idx + 1 + i) push(indices, base_idx + 1 + i)
push(indices, base_idx + 2 + i) push(indices, base_idx + 2 + i)
} }
} }
function add_square_cap(verts, indices, p, n, width, u, v_offset, v_scale, is_start, adjacent) { function add_square_cap(opts) {
var w = width * 0.5 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) var base_idx = length(verts)
// Direction along the line (away from adjacent point) // Direction along the line (away from adjacent point)
var dx = p.x - adjacent.x var dx = p.x - adjacent.x
var dy = p.y - adjacent.y var dy = p.y - adjacent.y
var len = math.sqrt(dx*dx + dy*dy) var len = math.sqrt(dx*dx + dy*dy)
if (len > 0.0001) { dx /= len; dy /= len } if (len > 0.0001) { dx /= len; dy /= len }
// Extend by half width // Extend by half width
var ext = w var ext = w
var ex = p.x + dx * ext var ex = p.x + dx * ext
var ey = p.y + dy * ext var ey = p.y + dy * ext
// Four corners of the cap // 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_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}) 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) return make_line(props)
}, },
line: function(x1, y1, x2, y2, props) { line: function(from, to, props) {
var p = 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) return make_line(p)
} }
} }

8
math.c
View File

@@ -10,7 +10,7 @@
// Utility function to get number from array index // Utility function to get number from array index
static double js_getnum_uint32(JSContext *js, JSValue v, unsigned int i) 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); double ret = js2number(js, val);
JS_FreeValue(js,val); JS_FreeValue(js,val);
return ret; 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) { static JSValue floats2array(JSContext *js, float *vals, size_t len) {
JSValue arr = JS_NewArray(js); JSValue arr = JS_NewArray(js);
for (size_t i = 0; i < len; i++) { 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; return arr;
} }
@@ -94,7 +94,7 @@ JSC_CCALL(math_norm,
JSValue newarr = JS_NewArray(js); JSValue newarr = JS_NewArray(js);
for (int i = 0; i < len; i++) 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; ret = newarr;
) )
@@ -238,7 +238,7 @@ JSC_CCALL(math_from_to,
int i = 0; int i = 0;
for (double val = start; val <= end; val += step) { for (double val = start; val <= end; val += step) {
if (val == end && !inclusive) break; if (val == end && !inclusive) break;
JS_SetPropertyUint32(js, jsarr, i++, number2js(js, val)); JS_SetPropertyNumber(js, jsarr, i++, number2js(js, val));
} }
return jsarr; return jsarr;

View File

@@ -69,7 +69,7 @@ static JSClassID js_mersenne_class_id;
static void js_mersenne_finalizer(JSRuntime *rt, JSValue val) { static void js_mersenne_finalizer(JSRuntime *rt, JSValue val) {
MTRand *mrand = JS_GetOpaque(val, js_mersenne_class_id); MTRand *mrand = JS_GetOpaque(val, js_mersenne_class_id);
js_free_rt(rt, mrand); js_free_rt(mrand);
} }
static JSClassDef js_mersenne_class = { static JSClassDef js_mersenne_class = {
@@ -131,7 +131,7 @@ static JSValue js_mersenne_use_call(JSContext *js, JSValueConst new_target, int
CELL_USE_INIT( CELL_USE_INIT(
JS_NewClassID(&js_mersenne_class_id); 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); JSValue proto = JS_NewObject(js);
JS_SetPropertyFunctionList(js, proto, js_mersenne_funcs, sizeof(js_mersenne_funcs)/sizeof(JSCFunctionListEntry)); JS_SetPropertyFunctionList(js, proto, js_mersenne_funcs, sizeof(js_mersenne_funcs)/sizeof(JSCFunctionListEntry));

View File

@@ -43,25 +43,31 @@ var emitters = {
// Update an emitter and its particles // Update an emitter and its particles
update: function(emitter, dt) { update: function(emitter, dt) {
// Spawn new particles // Spawn new particles
var pp = 0
if (emitter.rate > 0) { if (emitter.rate > 0) {
emitter.spawn_timer = (emitter.spawn_timer || 0) + dt emitter.spawn_timer = (emitter.spawn_timer || 0) + dt
var pp = 1 / emitter.rate pp = 1 / emitter.rate
while (emitter.spawn_timer > pp) { while (emitter.spawn_timer > pp) {
emitter.spawn_timer -= pp emitter.spawn_timer -= pp
emitters.spawn(emitter) emitters.spawn(emitter)
} }
} }
// Update existing particles // Update existing particles
for (var i = length(emitter.particles) - 1; i >= 0; i--) { var i = 0
var p = emitter.particles[i] 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.time += dt
p.pos.x += p.velocity.x * dt p.pos.x += p.velocity.x * dt
p.pos.y += p.velocity.y * dt p.pos.y += p.velocity.y * dt
// Scale animation // Scale animation
var grow_for = emitter.grow_for || 0.3 grow_for = emitter.grow_for || 0.3
var shrink_for = emitter.shrink_for || 0.5 shrink_for = emitter.shrink_for || 0.5
if (p.time < grow_for) { if (p.time < grow_for) {
p.scale = lerp(0, p.max_scale, p.time / grow_for) p.scale = lerp(0, p.max_scale, p.time / grow_for)
} else if (p.time > p.life - shrink_for) { } else if (p.time > p.life - shrink_for) {
@@ -69,9 +75,9 @@ var emitters = {
} else { } else {
p.scale = p.max_scale p.scale = p.max_scale
} }
// Alpha fade // Alpha fade
var alpha = 1 alpha = 1
if (p.time > p.life * 0.7) { if (p.time > p.life * 0.7) {
alpha = 1 - (p.time - p.life * 0.7) / (p.life * 0.3) alpha = 1 - (p.time - p.life * 0.7) / (p.life * 0.3)
} }

View File

@@ -82,46 +82,36 @@ PlaydateBackend.prototype.execute = function(commands) {
} }
PlaydateBackend.prototype.execute_command = function(cmd) { PlaydateBackend.prototype.execute_command = function(cmd) {
switch (cmd.cmd) { if (cmd.cmd == 'begin_render') {
case 'begin_render': this.cmd_begin_render(cmd)
this.cmd_begin_render(cmd) } else if (cmd.cmd == 'end_render') {
break this.cmd_end_render()
case 'end_render': } else if (cmd.cmd == 'set_camera') {
this.cmd_end_render() this.cmd_set_camera(cmd)
break } else if (cmd.cmd == 'draw_batch') {
case 'set_camera': this.cmd_draw_batch(cmd)
this.cmd_set_camera(cmd) } else if (cmd.cmd == 'shader_pass') {
break this.cmd_shader_pass(cmd) // DEGRADES
case 'draw_batch': } else if (cmd.cmd == 'apply_mask') {
this.cmd_draw_batch(cmd) this.cmd_apply_mask(cmd) // NATIVE!
break } else if (cmd.cmd == 'composite') {
case 'shader_pass': this.cmd_composite(cmd)
this.cmd_shader_pass(cmd) // DEGRADES } else if (cmd.cmd == 'blit') {
break this.cmd_blit(cmd)
case 'apply_mask': } else if (cmd.cmd == 'clear') {
this.cmd_apply_mask(cmd) // NATIVE! this.cmd_clear(cmd)
break } else if (cmd.cmd == 'present') {
case 'composite': // Nothing to do, display updates automatically
this.cmd_composite(cmd) } else {
break log.console(`Unknown command: ${cmd.cmd}`)
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}`)
} }
} }
PlaydateBackend.prototype.cmd_begin_render = function(cmd) { PlaydateBackend.prototype.cmd_begin_render = function(cmd) {
var target = cmd.target var target = cmd.target
var clear = cmd.clear var clear = cmd.clear
var pattern = null
if (target == 'screen') { if (target == 'screen') {
// Render to screen framebuffer // Render to screen framebuffer
this.current_target = 'screen' this.current_target = 'screen'
@@ -130,10 +120,10 @@ PlaydateBackend.prototype.cmd_begin_render = function(cmd) {
// Render to bitmap // Render to bitmap
this.current_target = target this.current_target = target
this.pd.graphics.pushContext(target.bitmap) this.pd.graphics.pushContext(target.bitmap)
if (clear) { if (clear) {
// Clear with color (Playdate is 1-bit, so color becomes pattern/dither) // 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.setDitherPattern(pattern)
this.pd.graphics.fillRect(0, 0, target.width, target.height) 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) { PlaydateBackend.prototype.cmd_draw_batch = function(cmd) {
var geometry = cmd.geometry var geometry = cmd.geometry
var material = cmd.material || {} var material = cmd.material || {}
// Get image (Playdate uses LCDBitmap for textures) // Get image (Playdate uses LCDBitmap for textures)
var image = this.get_image(material.texture || 'white') var image = this.get_image(material.texture || 'white')
if (!image) return if (!image) return
// Set draw mode based on material // Set draw mode based on material
var draw_mode = this.material_to_draw_mode(material) var draw_mode = this.material_to_draw_mode(material)
this.pd.graphics.setImageDrawMode(draw_mode) this.pd.graphics.setImageDrawMode(draw_mode)
// Draw each sprite in the batch // 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 // Each sprite is 2 triangles = 6 indices = 4 vertices
var vert_idx = geometry.indices[i] vert_idx = geometry.indices[i]
var v = geometry.vertices[vert_idx] v = geometry.vertices[vert_idx]
// Transform by camera // 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 // Get sprite size from vertices
var v2 = geometry.vertices[vert_idx + 2] v2 = geometry.vertices[vert_idx + 2]
var width = v2.pos[0] - v.pos[0] width = v2.pos[0] - v.pos[0]
var height = v2.pos[1] - v.pos[1] height = v2.pos[1] - v.pos[1]
// Transform size by camera zoom // Transform size by camera zoom
var scale = this.get_camera_scale(this.current_camera) scale = this.get_camera_scale(this.current_camera)
width *= scale width *= scale
height *= scale height *= scale
// Apply vertex color as dither pattern (best we can do on 1-bit) // Apply vertex color as dither pattern (best we can do on 1-bit)
if (v.color.a < 1.0) { 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) this.pd.graphics.setDitherPattern(alpha_pattern)
} }
// Draw image // Draw image
if (width != image.width || height != image.height) { if (width != image.width || height != image.height) {
// Need scaling // 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]) this.pd.graphics.drawBitmap(scaled, screen_pos[0], screen_pos[1])
} else { } else {
this.pd.graphics.drawBitmap(image, screen_pos[0], screen_pos[1]) 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) { PlaydateBackend.prototype.cmd_shader_pass = function(cmd) {
// NO SHADERS ON PLAYDATE // NO SHADERS ON PLAYDATE
// Degrade gracefully based on shader type // Degrade gracefully based on shader type
var shader = cmd.shader var shader = cmd.shader
var input = cmd.input var input = cmd.input
var params = cmd.params var params = cmd.params
if (shader == 'threshold') { if (shader == 'threshold') {
// Threshold: Just copy input (or could dither based on 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) this.copy_bitmap(input.bitmap, this.current_target.bitmap)
} }
else if (shader == 'gaussian_blur') { else if (shader == 'gaussian_blur') {
// Blur: Box blur in CPU (slow but possible) // 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) this.cpu_box_blur(input.bitmap, this.current_target.bitmap, params.radius || 5)
} }
else if (shader == 'add_textures') { else if (shader == 'add_textures') {
// Additive blend: Use XOR draw mode (not perfect but interesting) // 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.setImageDrawMode(this.pd.graphics.kDrawModeXOR)
this.pd.graphics.drawBitmap(input.bitmap, 0, 0) this.pd.graphics.drawBitmap(input.bitmap, 0, 0)
} }
else if (shader == 'crt_filter') { else if (shader == 'crt_filter') {
// CRT: Not possible, just copy // 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) this.copy_bitmap(input.bitmap, this.current_target.bitmap)
} }
else { else {
// Unknown shader: copy // 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) 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 content = cmd.content_texture.bitmap
var mask = cmd.mask_texture.bitmap var mask = cmd.mask_texture.bitmap
var invert = cmd.invert var invert = cmd.invert
var inverted = null
if (invert) { if (invert) {
// Invert mask first // 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.pushContext(inverted)
this.pd.graphics.setImageDrawMode(this.pd.graphics.kDrawModeInverted) this.pd.graphics.setImageDrawMode(this.pd.graphics.kDrawModeInverted)
this.pd.graphics.drawBitmap(mask, 0, 0) this.pd.graphics.drawBitmap(mask, 0, 0)
this.pd.graphics.popContext() this.pd.graphics.popContext()
mask = inverted mask = inverted
} }
// Set mask on content bitmap // Set mask on content bitmap
content.setMask(mask) content.setMask(mask)
// Draw masked content to current target // Draw masked content to current target
this.pd.graphics.drawBitmap(content, 0, 0) this.pd.graphics.drawBitmap(content, 0, 0)
// Clear mask (don't leave it set) // Clear mask (don't leave it set)
content.setMask(null) content.setMask(null)
} }
@@ -284,14 +285,15 @@ PlaydateBackend.prototype.cmd_blit = function(cmd) {
var bitmap = cmd.texture.bitmap var bitmap = cmd.texture.bitmap
var dst_rect = cmd.dst_rect var dst_rect = cmd.dst_rect
var filter = cmd.filter var filter = cmd.filter
// Scale bitmap to fit dst_rect // Scale bitmap to fit dst_rect
var scale_x = dst_rect.width / bitmap.width var scale_x = dst_rect.width / bitmap.width
var scale_y = dst_rect.height / bitmap.height var scale_y = dst_rect.height / bitmap.height
var scaled = null
if (scale_x != 1.0 || scale_y != 1.0) { if (scale_x != 1.0 || scale_y != 1.0) {
// Playdate only supports nearest-neighbor scaling // 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) this.pd.graphics.drawBitmap(scaled, dst_rect.x, dst_rect.y)
} else { } else {
this.pd.graphics.drawBitmap(bitmap, dst_rect.x, dst_rect.y) 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) { PlaydateBackend.prototype.cpu_box_blur = function(src, dst, radius) {
// Simple box blur implementation in CPU // Simple box blur implementation in CPU
// This is SLOW but correct // This is SLOW but correct
var width = src.width var width = src.width
var height = src.height var height = src.height
// Get pixel data (Playdate API for accessing bitmap pixels) // Get pixel data (Playdate API for accessing bitmap pixels)
var src_data = src.getData() var src_data = src.getData()
var dst_data = dst.getData() var dst_data = dst.getData()
var y = 0
for (var y = 0; y < height; y++) { var x = 0
for (var x = 0; x < width; x++) { var sum = 0
var sum = 0 var count = 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 // Sample neighborhood
for (var dy = -radius; dy <= radius; dy++) { for (dy = -radius; dy <= radius; dy++) {
for (var dx = -radius; dx <= radius; dx++) { for (dx = -radius; dx <= radius; dx++) {
var sx = x + dx sx = x + dx
var sy = y + dy sy = y + dy
if (sx >= 0 && sx < width && sy >= 0 && sy < height) { if (sx >= 0 && sx < width && sy >= 0 && sy < height) {
sum += src_data[sy * width + sx] sum += src_data[sy * width + sx]
count++ count++
} }
} }
} }
dst_data[y * width + x] = sum / count dst_data[y * width + x] = sum / count
} }
} }
dst.setData(dst_data) dst.setData(dst_data)
} }

View File

@@ -7,10 +7,10 @@ colorf js2color(JSContext *js,JSValue v) {
colorf color = {1,1,1,1}; // Default to white 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] // Handle array format: [r, g, b, a]
JSValue c[4]; 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.r = js2number(js,c[0]);
color.g = js2number(js,c[1]); color.g = js2number(js,c[1]);
@@ -31,10 +31,10 @@ colorf js2color(JSContext *js,JSValue v) {
JSValue color2js(JSContext *js, colorf color) JSValue color2js(JSContext *js, colorf color)
{ {
JSValue arr = JS_NewArray(js); JSValue arr = JS_NewArray(js);
JS_SetPropertyUint32(js, arr,0,number2js(js,(double)color.r)); JS_SetPropertyNumber(js, arr,0,number2js(js,(double)color.r));
JS_SetPropertyUint32(js, arr,1,number2js(js,(double)color.g)); JS_SetPropertyNumber(js, arr,1,number2js(js,(double)color.g));
JS_SetPropertyUint32(js, arr,2,number2js(js,(double)color.b)); JS_SetPropertyNumber(js, arr,2,number2js(js,(double)color.b));
JS_SetPropertyUint32(js, arr,3,number2js(js,(double)color.a)); JS_SetPropertyNumber(js, arr,3,number2js(js,(double)color.a));
return arr; return arr;
} }
@@ -43,9 +43,9 @@ HMM_Vec2 js2vec2(JSContext *js,JSValue v)
HMM_Vec2 v2; HMM_Vec2 v2;
// Check if it's an array // Check if it's an array
if (JS_IsArray(js, v)) { if (JS_IsArray(v)) {
{ JSValue val = JS_GetPropertyUint32(js,v,0); v2.X = js2number(js, val); JS_FreeValue(js,val); } { JSValue val = JS_GetPropertyNumber(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); } { JSValue val = JS_GetPropertyNumber(js,v,1); v2.Y = js2number(js, val); JS_FreeValue(js,val); }
} else { } else {
// Try to get x,y properties from object // Try to get x,y properties from object
JSValue x_val = JS_GetPropertyStr(js, v, "x"); 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 js2vec3(JSContext *js,JSValue v)
{ {
HMM_Vec3 v3; HMM_Vec3 v3;
{ JSValue val = JS_GetPropertyUint32(js, v,0); v3.x = 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_GetPropertyUint32(js, v,1); v3.y = 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_GetPropertyUint32(js, v,2); v3.z = 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; return v3;
} }
@@ -75,7 +75,7 @@ float *js2floats(JSContext *js, JSValue v, size_t *len)
*len = JS_ArrayLength(js,v); *len = JS_ArrayLength(js,v);
float *arr = malloc(sizeof(float)* *len); float *arr = malloc(sizeof(float)* *len);
for (int i = 0; i < *len; i++) 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; return arr;
} }
@@ -84,14 +84,14 @@ double *js2doubles(JSContext *js, JSValue v, size_t *len)
*len = JS_ArrayLength(js,v); *len = JS_ArrayLength(js,v);
double *arr = malloc(sizeof(double)* *len); double *arr = malloc(sizeof(double)* *len);
for (int i = 0; i < *len; i++) 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; return arr;
} }
HMM_Vec3 js2vec3f(JSContext *js, JSValue v) HMM_Vec3 js2vec3f(JSContext *js, JSValue v)
{ {
HMM_Vec3 vec; HMM_Vec3 vec;
if (JS_IsArray(js, v)) if (JS_IsArray(v))
return js2vec3(js,v); return js2vec3(js,v);
else else
vec.x = vec.y = vec.z = js2number(js,v); 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 vec32js(JSContext *js, HMM_Vec3 v)
{ {
JSValue array = JS_NewArray(js); JSValue array = JS_NewArray(js);
JS_SetPropertyUint32(js, array,0,number2js(js,v.x)); JS_SetPropertyNumber(js, array,0,number2js(js,v.x));
JS_SetPropertyUint32(js, array,1,number2js(js,v.y)); JS_SetPropertyNumber(js, array,1,number2js(js,v.y));
JS_SetPropertyUint32(js, array,2,number2js(js,v.z)); JS_SetPropertyNumber(js, array,2,number2js(js,v.z));
return array; return array;
} }
@@ -115,10 +115,10 @@ JSValue vec3f2js(JSContext *js, HMM_Vec3 v)
JSValue quat2js(JSContext *js, HMM_Quat q) JSValue quat2js(JSContext *js, HMM_Quat q)
{ {
JSValue arr = JS_NewArray(js); JSValue arr = JS_NewArray(js);
JS_SetPropertyUint32(js, arr, 0, number2js(js,q.x)); JS_SetPropertyNumber(js, arr, 0, number2js(js,q.x));
JS_SetPropertyUint32(js, arr,1,number2js(js,q.y)); JS_SetPropertyNumber(js, arr,1,number2js(js,q.y));
JS_SetPropertyUint32(js, arr,2,number2js(js,q.z)); JS_SetPropertyNumber(js, arr,2,number2js(js,q.z));
JS_SetPropertyUint32(js, arr,3,number2js(js,q.w)); JS_SetPropertyNumber(js, arr,3,number2js(js,q.w));
return arr; return arr;
} }
@@ -126,7 +126,7 @@ HMM_Vec4 js2vec4(JSContext *js, JSValue v)
{ {
HMM_Vec4 v4; HMM_Vec4 v4;
for (int i = 0; i < 4; i++) 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; return v4;
} }
@@ -141,7 +141,7 @@ double arr_vec_length(JSContext *js,JSValue v)
double sum = 0; double sum = 0;
for (int i = 0; i < len; i++) 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); return sqrt(sum);
} }
@@ -155,7 +155,7 @@ JSValue vec42js(JSContext *js, HMM_Vec4 v)
{ {
JSValue array = JS_NewArray(js); JSValue array = JS_NewArray(js);
for (int i = 0; i < 4; i++) 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; return array;
} }
@@ -165,7 +165,7 @@ HMM_Vec2 *js2cpvec2arr(JSContext *js,JSValue v) {
arrsetlen(arr,n); arrsetlen(arr,n);
for (int i = 0; i < n; i++) { 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); arr[i] = js2vec2(js,ii);
JS_FreeValue(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) { static JSValue floats2array(JSContext *js, float *vals, size_t len) {
JSValue arr = JS_NewArray(js); JSValue arr = JS_NewArray(js);
for (size_t i = 0; i < len; i++) { 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; return arr;
} }
@@ -211,15 +211,15 @@ lrtb js2lrtb(JSContext *js, JSValue v)
JSValue vec22js(JSContext *js,HMM_Vec2 v) JSValue vec22js(JSContext *js,HMM_Vec2 v)
{ {
JSValue array = JS_NewArray(js); JSValue array = JS_NewArray(js);
JS_SetPropertyUint32(js, array,0,number2js(js,v.x)); JS_SetPropertyNumber(js, array,0,number2js(js,v.x));
JS_SetPropertyUint32(js, array,1,number2js(js,v.y)); JS_SetPropertyNumber(js, array,1,number2js(js,v.y));
return array; return array;
} }
JSValue vecarr2js(JSContext *js,HMM_Vec2 *points, int n) { JSValue vecarr2js(JSContext *js,HMM_Vec2 *points, int n) {
JSValue array = JS_NewArray(js); JSValue array = JS_NewArray(js);
for (int i = 0; i < n; i++) 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; return array;
} }

View File

@@ -7,88 +7,106 @@ var math = use('math')
var rasterize = {} var rasterize = {}
function within_wedge(dx, dy, start, end, full_circle) { function within_wedge(dx, dy, wedge) {
if (full_circle) return true if (wedge.full) return true
var ang = math.arc_tangent(dy, dx) var ang = math.arc_tangent(dy, dx)
if (ang < 0) ang += pi * 2 if (ang < 0) ang += pi * 2
var t = ang / (pi * 2) var t = ang / (pi * 2)
if (start <= end) return t >= start && t <= end if (wedge.start <= wedge.end) return t >= wedge.start && t <= wedge.end
return t >= start || t <= end return t >= wedge.start || t <= wedge.end
} }
rasterize.ellipse = function ellipse(pos, radii, opt) { rasterize.ellipse = function ellipse(pos, radii, opt) {
opt = opt || {} var _opt = opt || {}
var rx = radii[0], ry = radii[1] var rx = radii[0], ry = radii[1]
if (rx <= 0 || ry <= 0) return [] if (rx <= 0 || ry <= 0) return []
var cx = pos[0], cy = pos[1] var cx = pos[0], cy = pos[1]
var raw_start = opt.start || 0 var raw_start = _opt.start || 0
var raw_end = opt.end || 1 var raw_end = _opt.end || 1
var full_circle = abs(raw_end - raw_start) >= 1 - 1e-9 var full_circle = abs(raw_end - raw_start) >= 1 - 1e-9
var start = (raw_start % 1 + 1) % 1 var start = (raw_start % 1 + 1) % 1
var end = (raw_end % 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, var rx_i = rx - thickness,
ry_i = ry - thickness ry_i = ry - thickness
var hole = (rx_i > 0 && ry_i > 0) var hole = (rx_i > 0 && ry_i > 0)
if (!hole && thickness == 1) { var points = []
var points = [] var rx_sq = rx * rx, ry_sq = ry * ry
var rx_sq = rx * rx, ry_sq = ry * ry var two_rx_sq = null
var two_rx_sq = rx_sq << 1, two_ry_sq = ry_sq << 1 var two_ry_sq = null
var x = 0, y = ry, px = 0, py = two_rx_sq * y var x = 0, y = 0, px = 0, py = 0
var p = ry_sq - rx_sq * ry + 0.25 * rx_sq var p = 0
function add_pts(x, y) { var add_pts = function(ax, ay) {
var pts = [ var pts = [
[cx + x, cy + y], [cx - x, cy + y], [cx + ax, cy + ay], [cx - ax, cy + ay],
[cx + x, cy - y], [cx - x, cy - y] [cx + ax, cy - ay], [cx - ax, cy - ay]
] ]
points = array(points, filter(pts, pt => within_wedge(pt[0]-cx, pt[1]-cy, start, end, full_circle))) 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) { while (px < py) {
add_pts(x, y) add_pts(x, y)
++x; px += two_ry_sq x += 1; px += two_ry_sq
if (p < 0) p += ry_sq + px 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 p = ry_sq*(x+.5)*(x+.5) + rx_sq*(y-1)*(y-1) - rx_sq*ry_sq
while (y >= 0) { while (y >= 0) {
add_pts(x, y) add_pts(x, y)
--y; py -= two_rx_sq y -= 1; py -= two_rx_sq
if (p > 0) p += rx_sq - py 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} return {type: 'points', data: points}
} }
var strips = [] 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 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) { for (dy = -ry; dy <= ry; ++dy) {
var yy = dy * dy yy = dy * dy
var x_out = floor(rx * math.sqrt(1 - yy / ry_sq)) x_out = floor(rx * math.sqrt(1 - yy / ry_sq))
var y_screen = cy + dy 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 run_start = null
for (var dx = -x_out; dx <= x_out; ++dx) { for (dx = -x_out; dx <= x_out; ++dx) {
if (hole && abs(dx) <= x_in) { run_start = null; continue } 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 if (run_start == null) run_start = cx + dx
var last = (dx == x_out) last = (dx == x_out)
var next_in_ring = next_in_ring =
!last && !last &&
!(hole && abs(dx+1) <= x_in) && !(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) { if (last || !next_in_ring) {
push(strips, { push(strips, {
@@ -134,18 +152,18 @@ rasterize.outline_rect = function outline_rect(rect, thickness) {
} }
rasterize.round_rect = function round_rect(rect, radius, thickness) { rasterize.round_rect = function round_rect(rect, radius, thickness) {
thickness = thickness || 1 var _thickness = thickness || 1
if (thickness <= 0) { if (_thickness <= 0) {
return rasterize.fill_round_rect(rect, radius) 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 || if ((_thickness << 1) >= rect.width ||
(thickness << 1) >= rect.height || (_thickness << 1) >= rect.height ||
thickness >= radius) { _thickness >= _radius) {
return rasterize.fill_round_rect(rect, radius) return rasterize.fill_round_rect(rect, _radius)
} }
var x0 = rect.x, var x0 = rect.x,
@@ -153,27 +171,32 @@ rasterize.round_rect = function round_rect(rect, radius, thickness) {
x1 = rect.x + rect.width - 1, x1 = rect.x + rect.width - 1,
y1 = rect.y + rect.height - 1 y1 = rect.y + rect.height - 1
var cx_l = x0 + radius, cx_r = x1 - radius var cx_l = x0 + _radius, cx_r = x1 - _radius
var cy_t = y0 + radius, cy_b = y1 - radius var cy_t = y0 + _radius, cy_b = y1 - _radius
var r_out = radius var r_out = _radius
var r_in = radius - thickness var r_in = _radius - _thickness
var rects = [ var rects = [
{ x:x0 + radius, y:y0, width:rect.width - (radius << 1), height:thickness }, { 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 + _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: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:x1 - _thickness + 1, y:y0 + _radius, width:_thickness, height:rect.height - (_radius << 1) }
] ]
var strips = [] 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) { for (dy = 0; dy < _radius; ++dy) {
var dy_sq = dy * dy dy_sq = dy * dy
var dx_out = floor(math.sqrt(r_out * r_out - dy_sq)) dx_out = floor(math.sqrt(r_out * r_out - dy_sq))
var dx_in = (r_in > 0 && dy < r_in) dx_in = (r_in > 0 && dy < r_in)
? floor(math.sqrt(r_in * r_in - dy_sq)) ? floor(math.sqrt(r_in * r_in - dy_sq))
: -1 : -1
var w = dx_out - dx_in w = dx_out - dx_in
if (w <= 0) continue if (w <= 0) continue
push(strips, 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) { 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, var x0 = rect.x,
y0 = rect.y, y0 = rect.y,
@@ -196,20 +219,23 @@ rasterize.fill_round_rect = function fill_round_rect(rect, radius) {
y1 = rect.y + rect.height - 1 y1 = rect.y + rect.height - 1
var rects = [ var rects = [
{ x:x0 + radius, y:y0, width:rect.width - (radius << 1), height:rect.height }, { 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: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:x1 - _radius + 1, y:y0 + _radius, width:_radius, height:rect.height - (_radius << 1) }
] ]
var cx_l = x0 + radius, cx_r = x1 - radius var cx_l = x0 + _radius, cx_r = x1 - _radius
var cy_t = y0 + radius, cy_b = y1 - radius var cy_t = y0 + _radius, cy_b = y1 - _radius
var caps = [] var caps = []
var dy = 0
var dx = 0
var w = 0
for (var dy = 0; dy < radius; ++dy) { for (dy = 0; dy < _radius; ++dy) {
var dx = floor(math.sqrt(radius * radius - dy * dy)) dx = floor(math.sqrt(_radius * _radius - dy * dy))
var w = (dx << 1) + 1 w = (dx << 1) + 1
push(caps, push(caps,
{ x:cx_l - dx, y:cy_t - dy, width:w, height:1 }, { 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_r - dx, y:cy_t - dy, width:w, height:1 },
{ x:cx_l - dx, y:cy_b + dy, width:w, height:1 }, { x:cx_l - dx, y:cy_b + dy, width:w, height:1 },

View File

@@ -41,20 +41,22 @@ function isRecognizedExtension(ext) {
return false return false
} }
function find_in_path(filename, exts = []) { function find_in_path(filename, exts) {
var _exts = exts || []
if (!is_text(filename)) return null if (!is_text(filename)) return null
var candidate = null
if (search(filename, '.') != null) { if (search(filename, '.') != null) {
var candidate = filename // possibly need "/" ? candidate = filename // possibly need "/" ?
if (io.exists(candidate) && !io.is_directory(candidate)) return candidate if (io.exists(candidate) && !io.is_directory(candidate)) return candidate
return null return null
} }
// Only check extensions if exts is provided and not empty // Only check extensions if exts is provided and not empty
if (length(exts) > 0) { var cand = null
var cand = null if (length(_exts) > 0) {
arrfor(exts, function(ext) { arrfor(_exts, function(ext) {
var candidate = filename + '.' + ext candidate = filename + '.' + ext
if (io.exists(candidate) && !io.is_directory(candidate)){ if (io.exists(candidate) && !io.is_directory(candidate)){
cand = candidate cand = candidate
return true return true
@@ -63,7 +65,7 @@ function find_in_path(filename, exts = []) {
if (cand != null) return cand if (cand != null) return cand
} else { } else {
// Fallback to extensionless file only if no extensions are specified // 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 if (io.exists(candidate) && !io.is_directory(candidate)) return candidate
} }
return null return null
@@ -95,12 +97,13 @@ Resources.find_font = hashify(function(file) {
function read_ignore(dir) { function read_ignore(dir) {
var path = dir + '/.prosperonignore' var path = dir + '/.prosperonignore'
var patterns = [] var patterns = []
var lines = null
if (io.exists(path)) { if (io.exists(path)) {
var lines = array(io.slurp(path), '\n') lines = array(io.slurp(path), '\n')
arrfor(lines, function(line) { arrfor(lines, function(line) {
line = trim(line) var trimmed = trim(line)
if (!line || starts_with(line, '#')) return if (!trimmed || starts_with(trimmed, '#')) return
push(patterns, line) push(patterns, trimmed)
}) })
} }
return patterns return patterns
@@ -108,20 +111,24 @@ function read_ignore(dir) {
// Return a list of recognized files in the directory (and subdirectories), // Return a list of recognized files in the directory (and subdirectories),
// skipping those matched by .prosperonignore. Directory paths are skipped. // skipping those matched by .prosperonignore. Directory paths are skipped.
Resources.getAllFiles = function(dir = "") { Resources.getAllFiles = function(dir) {
var patterns = read_ignore(dir) var _dir = dir || ""
var all = io.globfs(patterns, dir) var patterns = read_ignore(_dir)
var all = io.globfs(patterns, _dir)
var results = [] var results = []
arrfor(all, function(f) { arrfor(all, function(f) {
var fullPath = dir + '/' + f var fullPath = _dir + '/' + f
try { var _stat = function() {
var st = io.stat(fullPath) var st = io.stat(fullPath)
// skip directories (filesize=0) or unrecognized extension // skip directories (filesize=0) or unrecognized extension
if (!st.filesize) return if (!st.filesize) return
var ext = getExtension(f) var ext = getExtension(f)
if (!isRecognizedExtension(ext)) return if (!isRecognizedExtension(ext)) return
push(results, fullPath) push(results, fullPath)
} catch(e) {} } disruption {
// skip files that can't be stat'd
}
_stat()
}) })
return results return results
} }

1022
sdl_gpu.cm

File diff suppressed because it is too large Load Diff

View File

@@ -35,16 +35,16 @@ var player = soundwave.create({
// Load and cache PCM data from a file path // Load and cache PCM data from a file path
audio.pcm = function pcm(file) { audio.pcm = function pcm(file) {
file = res.find_sound(file) var path = res.find_sound(file)
if (!file) return null if (!path) return null
// Check player's cache first // Check player's cache first
if (player.pcm_cache[file]) return player.pcm_cache[file] if (player.pcm_cache[path]) return player.pcm_cache[path]
var buf = io.slurp(file) var buf = io.slurp(path)
if (!buf) return null if (!buf) return null
return player.decode(buf, file) return player.decode(buf, path)
} }
// Play a sound file, returns voice object // Play a sound file, returns voice object
@@ -73,11 +73,12 @@ feeder.resume_device()
// Audio pump - called periodically to fill the audio buffer // Audio pump - called periodically to fill the audio buffer
function pump() { function pump() {
var mixed = null
while (feeder.queued() < CHUNK_BYTES * 3) { while (feeder.queued() < CHUNK_BYTES * 3) {
var mixed = player.pull(FRAMES_PER_CHUNK) mixed = player.pull(FRAMES_PER_CHUNK)
feeder.put(mixed) feeder.put(mixed)
} }
$delay(pump, 1/240) $delay(pump, 1/240)
} }

View File

@@ -46,13 +46,7 @@ void sprite_apply(sprite *sp)
// Sprite class definitions // Sprite class definitions
static JSClassID js_sprite_id; 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,) QJSCLASSMARK(sprite,)
// SPRITE ACTION FUNCTIONS // 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->layer, argv[0], layer, number)
JS_GETATOM(js, sp->color, argv[0], color, color) 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)) { if (!JS_IsNull(image)) {
sp->image = image; // Transfer ownership, no need to dup sp->image = image; // Transfer ownership, no need to dup
} }

View File

@@ -2,20 +2,22 @@
var Anim = (() => { var Anim = (() => {
def DEFAULT_MIN = 1 / 60; /* 16 ms one frame */ 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 { return {
src : source, src : source,
idx : 0, idx : 0,
timer : 0, timer : 0,
loop : loop ?? source.loop ?? true loop : local_loop
}; };
} }
function update(a, dt){ function update(a, dt){
a.timer += dt; a.timer += dt;
def frames = a.src.frames; var frames = a.src.frames;
var time = null;
while(true){ 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 */ if(a.timer < time) break; /* still on current frame */
a.timer -= time; a.timer -= time;

View File

@@ -52,10 +52,14 @@ function hsl_to_rgb(h, s, l) {
var bunny_count = 20 var bunny_count = 20
for (var i = 0; i < bunny_count; i++) { var i = 0;
var pct = i/bunny_count var pct = 0;
var hue = 270 * i / bunny_count var hue = 0;
var sp = sprite.create(bunny, { 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], pos: [random.random()*dim.x*pct, random.random()*dim.y*pct],
center, center,
color: hsl_to_rgb(hue, 0.5, 0.5) color: hsl_to_rgb(hue, 0.5, 0.5)

View File

@@ -18,8 +18,10 @@ log.console("\nLooking for different colorspaces in supported formats...");
// Group formats by colorspace // Group formats by colorspace
var colorspaces = {}; var colorspaces = {};
for (var i = 0; i < length(formats); i++) { var i = 0;
var fmt = formats[i]; var fmt = null;
for (i = 0; i < length(formats); i++) {
fmt = formats[i];
if (!colorspaces[fmt.colorspace]) { if (!colorspaces[fmt.colorspace]) {
colorspaces[fmt.colorspace] = []; colorspaces[fmt.colorspace] = [];
} }
@@ -53,8 +55,9 @@ arrfor(array(colorspaces), function(cs) {
}; };
var cam = camera.open(cam_id, custom_format); var cam = camera.open(cam_id, custom_format);
var actual = null;
if (cam) { if (cam) {
var actual = cam.get_format(); actual = cam.get_format();
log.console(" Opened successfully!"); log.console(" Opened successfully!");
log.console(" Actual colorspace: " + actual.colorspace); log.console(" Actual colorspace: " + actual.colorspace);

View File

@@ -40,67 +40,76 @@ $receiver(e => {
}); });
// Wait for approval then capture // Wait for approval then capture
var srgb_surf = null;
var linear_surf = null;
var jpeg_surf = null;
var hd_surf = null;
function capture_test() { function capture_test() {
if (!approved) { if (!approved) {
$delay(capture_test, 0.1); $delay(capture_test, 0.1);
return; return;
} }
log.console("\nCapturing frame..."); log.console("\nCapturing frame...");
var surf = cam.capture(); var surf = cam.capture();
if (!surf) { if (!surf) {
log.console("No frame captured yet, retrying..."); log.console("No frame captured yet, retrying...");
$delay(capture_test, 0.1); $delay(capture_test, 0.1);
return; return;
} }
log.console("\nCaptured surface:"); log.console("\nCaptured surface:");
log.console(" Size:", surf.width + "x" + surf.height); log.console(" Size:", surf.width + "x" + surf.height);
log.console(" Format:", surf.format); log.console(" Format:", surf.format);
// Test various colorspace conversions // Test various colorspace conversions
log.console("\nTesting colorspace conversions:"); log.console("\nTesting colorspace conversions:");
// Convert to sRGB if not already // 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") { if (format.colorspace != "srgb") {
try { convert_srgb();
var srgb_surf = surf.convert(surf.format, "srgb");
log.console(" Converted to sRGB colorspace");
} catch(e) {
log.console(" sRGB conversion failed:", e.message);
}
} }
// Convert to linear sRGB for processing // Convert to linear sRGB for processing
try { var convert_linear = function() {
var linear_surf = surf.convert("rgba8888", "srgb_linear"); linear_surf = surf.convert("rgba8888", "srgb_linear");
log.console(" Converted to linear sRGB (RGBA8888) for processing"); log.console(" Converted to linear sRGB (RGBA8888) for processing");
} catch(e) { } disruption {
log.console(" Linear sRGB conversion failed:", e.message); log.console(" Linear sRGB conversion failed");
} }
convert_linear();
// Convert to JPEG colorspace (common for compression) // Convert to JPEG colorspace (common for compression)
try { var convert_jpeg = function() {
var jpeg_surf = surf.convert("rgb888", "jpeg"); jpeg_surf = surf.convert("rgb888", "jpeg");
log.console(" Converted to JPEG colorspace (RGB888) for compression"); log.console(" Converted to JPEG colorspace (RGB888) for compression");
} catch(e) { } disruption {
log.console(" JPEG colorspace conversion failed:", e.message); log.console(" JPEG colorspace conversion failed");
} }
convert_jpeg();
// If YUV format, try BT.709 (HD video standard) // If YUV format, try BT.709 (HD video standard)
if (search(surf.format, "yuv") != null || search(surf.format, "yuy") != null) { var convert_hd = function() {
try { hd_surf = surf.convert(surf.format, "bt709_limited");
var hd_surf = surf.convert(surf.format, "bt709_limited"); log.console(" Converted to BT.709 limited (HD video standard)");
log.console(" Converted to BT.709 limited (HD video standard)"); } disruption {
} catch(e) { log.console(" BT.709 conversion failed");
log.console(" BT.709 conversion failed:", e.message);
}
} }
if (search(surf.format, "yuv") != null || search(surf.format, "yuy") != null) {
convert_hd();
}
log.console("\nTest complete!"); log.console("\nTest complete!");
$stop(); $stop();
} }
// Start capture test after a short delay // Start capture test after a short delay
$delay(capture_test, 0.5); $delay(capture_test, 0.5);

View File

@@ -10,23 +10,32 @@ var cameras = camera.list();
log.console("Found", length(cameras), "cameras"); log.console("Found", length(cameras), "cameras");
// Get info about each camera // Get info about each camera
for (var i = 0; i < length(cameras); i++) { var i = 0;
var cam_id = cameras[i]; 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("\nCamera", i + 1, "ID:", cam_id);
log.console(" Name:", camera.name(cam_id)); log.console(" Name:", camera.name(cam_id));
log.console(" Position:", camera.position(cam_id)); log.console(" Position:", camera.position(cam_id));
// Get supported formats // Get supported formats
var formats = camera.supported_formats(cam_id); formats = camera.supported_formats(cam_id);
log.console(" Supported formats:", length(formats)); log.console(" Supported formats:", length(formats));
// Show first few formats // Show first few formats
for (var j = 0; j < length(formats); j++) { for (j = 0; j < length(formats); j++) {
var fmt = formats[j]; fmt = formats[j];
log.console(" Format", j + 1 + ":"); log.console(" Format", j + 1 + ":");
log.console(" Pixel format:", fmt.format); log.console(" Pixel format:", fmt.format);
log.console(" Resolution:", fmt.width + "x" + fmt.height); 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) + ")"); "(" + (fmt.framerate_numerator / fmt.framerate_denominator) + ")");
log.console(" Colorspace:", fmt.colorspace); 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 // Open the first camera with a specific format if available
if (length(cameras) > 0) { if (length(cameras) > 0) {
log.console("\nOpening first camera..."); log.console("\nOpening first camera...");
var cam_id = cameras[0]; cam_id = cameras[0];
var formats = camera.supported_formats(cam_id); formats = camera.supported_formats(cam_id);
// Try to find a 640x480 format // Try to find a 640x480 format
var preferred_format = null; preferred_format = null;
for (var i = 0; i < length(formats); i++) { for (i = 0; i < length(formats); i++) {
if (formats[i].width == 640 && formats[i].height == 480) { if (formats[i].width == 640 && formats[i].height == 480) {
preferred_format = formats[i]; preferred_format = formats[i];
break; break;
} }
} }
var cam; cam = null;
if (preferred_format) { if (preferred_format) {
log.console("Opening with 640x480 format..."); log.console("Opening with 640x480 format...");
cam = camera.open(cam_id, preferred_format); cam = camera.open(cam_id, preferred_format);
@@ -55,21 +64,21 @@ if (length(cameras) > 0) {
log.console("Opening with default format..."); log.console("Opening with default format...");
cam = camera.open(cam_id); cam = camera.open(cam_id);
} }
if (cam) { if (cam) {
log.console("Camera opened successfully!"); log.console("Camera opened successfully!");
log.console("Driver being used:", cam.get_driver()); log.console("Driver being used:", cam.get_driver());
// Get the actual format being used // 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("Actual format being used:");
log.console(" Pixel format:", actual_format.format); log.console(" Pixel format:", actual_format.format);
log.console(" Resolution:", actual_format.width + "x" + actual_format.height); log.console(" Resolution:", actual_format.width + "x" + actual_format.height);
log.console(" FPS:", actual_format.framerate_numerator + "/" + actual_format.framerate_denominator, log.console(" FPS:", actual_format.framerate_numerator + "/" + actual_format.framerate_denominator,
"(" + (actual_format.framerate_numerator / actual_format.framerate_denominator) + ")"); "(" + (actual_format.framerate_numerator / actual_format.framerate_denominator) + ")");
log.console(" Colorspace:", actual_format.colorspace); log.console(" Colorspace:", actual_format.colorspace);
// Clean up - camera will be closed when object is freed // Clean up - camera will be closed when object is freed
cam = null; cam = null;
} }
} }

View File

@@ -1,6 +1,6 @@
// Test draw2d module without moth framework // Test draw2d module without moth framework
var draw2d var draw2d = null
var graphics var graphics = null
var os = use('os'); var os = use('os');
var input = use('input') var input = use('input')
var math = use('math/radians') var math = use('math/radians')
@@ -170,11 +170,16 @@ function start_drawing() {
// Draw some points in a pattern // Draw some points in a pattern
var point_count = 20; var point_count = 20;
for (var i = 0; i < point_count; i++) { var i = 0;
var angle = (i / point_count) * pi * 2; var angle = 0;
var r = 30 + math.sine(t * 4 + i * 0.5) * 10; var r = 0;
var px = 650 + math.cosine(angle) * r; var px = 0;
var py = 300 + math.sine(angle) * r; 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( draw2d.point(
[px, py], [px, py],

View File

@@ -31,13 +31,17 @@ var colorspaces = ["srgb", "srgb_linear", "jpeg", "bt601_limited", "bt709_limite
var test_format = "rgba8888"; var test_format = "rgba8888";
log.console("\nTest 3: Converting to", test_format, "with different colorspaces:"); log.console("\nTest 3: Converting to", test_format, "with different colorspaces:");
for (var i = 0; i < length(colorspaces); i++) { var i = 0;
try { var conv = null;
var conv = surf.convert(test_format, colorspaces[i]); 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"); log.console(" " + colorspaces[i] + ": Success");
} catch(e) { } disruption {
log.console(" " + colorspaces[i] + ": Failed -", e.message); log.console(" " + colorspaces[i] + ": Failed");
} }
try_convert();
} }
// Test 4: YUV formats with appropriate colorspaces // Test 4: YUV formats with appropriate colorspaces
@@ -49,15 +53,18 @@ var yuv_tests = [
{format: "yvyu", colorspace: "bt601_full"} {format: "yvyu", colorspace: "bt601_full"}
]; ];
for (var i = 0; i < length(yuv_tests); i++) { var test = null;
var test = yuv_tests[i]; var try_yuv = null;
try { for (i = 0; i < length(yuv_tests); i++) {
var conv = surf.convert(test.format, test.colorspace); test = yuv_tests[i];
try_yuv = function() {
conv = surf.convert(test.format, test.colorspace);
log.console(" " + test.format + " with " + test.colorspace + ": Success"); log.console(" " + test.format + " with " + test.colorspace + ": Success");
} catch(e) { } disruption {
log.console(" " + test.format + " with " + test.colorspace + ": Failed -", e.message); log.console(" " + test.format + " with " + test.colorspace + ": Failed");
} }
try_yuv();
} }
log.console("\nColorspace conversion test complete!"); log.console("\nColorspace conversion test complete!");
$stop(); $stop();

View File

@@ -1,6 +1,6 @@
// Test webcam display // Test webcam display
var draw2d var draw2d = null
var graphics var graphics = null
var os = use('os'); var os = use('os');
var input = use('input') var input = use('input')
var json = use('json') var json = use('json')
@@ -88,7 +88,8 @@ send(video_actor, {
// Look for a 640x480 format with preferred colorspace // Look for a 640x480 format with preferred colorspace
var preferred_format = null; 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) { if (formats[i].width == 640 && formats[i].height == 480) {
preferred_format = formats[i]; preferred_format = formats[i];
// Prefer JPEG or sRGB colorspace if available // Prefer JPEG or sRGB colorspace if available

View File

@@ -182,16 +182,6 @@ transform mat2transform(HMM_Mat4 m)
// Transform class definitions // Transform class definitions
JSClassID js_transform_id; 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,) 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++) { 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]); JS_FreeValue(js,cur_parent->jschildren[i]);
arrdelswap(cur_parent->jschildren,i); arrdelswap(cur_parent->jschildren,i);
break; break;
@@ -341,7 +331,7 @@ JSC_CCALL(transform_array,
HMM_Mat4 m= transform2mat(t); HMM_Mat4 m= transform2mat(t);
ret = JS_NewArray(js); ret = JS_NewArray(js);
for (int i = 0; i < 16; i++) 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, JSC_CCALL(transform_torect,
@@ -353,7 +343,7 @@ JSC_CCALL(transform_children,
transform *t = js2transform(js,self); transform *t = js2transform(js,self);
ret = JS_NewArray(js); ret = JS_NewArray(js);
for (int i = 0; i < arrlen(t->jschildren); i++) 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[] = { static const JSCFunctionListEntry js_transform_funcs[] = {

View File

@@ -14,11 +14,12 @@ function make_engine(default_clock) {
this.tweens = filter(this.tweens, t => t != tween) this.tweens = filter(this.tweens, t => t != tween)
}, },
update(current_time) { update(current_time) {
if (current_time == null) { var ct = current_time
current_time = this.default_clock ? this.default_clock() : time.number() if (ct == null) {
ct = this.default_clock ? this.default_clock() : time.number()
} }
arrfor(this.tweens, function(tween) { arrfor(this.tweens, function(tween) {
tween._update(current_time) tween._update(ct)
}) })
}, },
clear() { clear() {
@@ -47,7 +48,7 @@ var TweenProto = {
if (is_object(value)) { if (is_object(value)) {
arrfor(array(value), subkey => { arrfor(array(value), subkey => {
var flatKey = key + '.' + 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] this.endVals[flatKey] = value[subkey]
}) })
} else { } else {
@@ -87,7 +88,7 @@ var TweenProto = {
_update: function(now) { _update: function(now) {
this.seek(now) this.seek(now)
this.onUpdateCallback?.() if (this.onUpdateCallback) this.onUpdateCallback()
}, },
seek: function(global_time) { seek: function(global_time) {
@@ -99,11 +100,14 @@ var TweenProto = {
var start = this.startVals[key] var start = this.startVals[key]
var end = this.endVals[key] var end = this.endVals[key]
var value = start + (end - start) * eased var value = start + (end - start) * eased
var parts = null
var objKey = null
var subKey = null
if (search(key, '.') != null) { if (search(key, '.') != null) {
var parts = array(key, '.') parts = array(key, '.')
var objKey = parts[0] objKey = parts[0]
var subKey = parts[1] subKey = parts[1]
if (!this.obj[objKey]) { if (!this.obj[objKey]) {
this.obj[objKey] = {} this.obj[objKey] = {}