This commit is contained in:
2026-01-16 20:56:16 -06:00
parent 9f6435ece9
commit 22962bbd63
33 changed files with 643 additions and 315 deletions

View File

@@ -29,8 +29,8 @@ action.get_icon_for_action = function(action)
var primary_binding = bindings[0]
if (this.current_device == 'keyboard') {
if (primary_binding.startsWith('mouse_button_')) {
var button = primary_binding.replace('mouse_button_', '')
if (starts_with(primary_binding, 'mouse_button_')) {
var button = replace(primary_binding, 'mouse_button_', '')
return 'ui/mouse/mouse_' + button + '.png'
} else {
// Handle special keyboard keys
@@ -132,10 +132,10 @@ default_action_map.accion = 'y'
// Utility to detect device from input id
function detect_device(input_id) {
if (input_id.startsWith('gamepad_')) return 'gamepad'
if (input_id.startsWith('swipe_')) return 'touch'
if (input_id.startsWith('mouse_button')) return 'keyboard' // Mouse buttons are part of keyboard/mouse controller
if (input_id.startsWith('touch_')) return 'touch'
if (starts_with(input_id, 'gamepad_')) return 'gamepad'
if (starts_with(input_id, 'swipe_')) return 'touch'
if (starts_with(input_id, 'mouse_button')) return 'keyboard' // Mouse buttons are part of keyboard/mouse controller
if (starts_with(input_id, 'touch_')) return 'touch'
return 'keyboard'
}
@@ -171,7 +171,7 @@ action.current_gamepad_type = null
// Copy defaults
for (var key in default_action_map) {
action.action_map[key] = default_action_map[key].slice()
action.action_map[key] = array(default_action_map[key])
}
// Swiperecognizer state & tuning
@@ -181,14 +181,14 @@ var SWIPE_MAX_TIME = 500 // ms
action.on_input = function(action_id, evt)
{
// 1) Detect & store which device user is on (only from raw input, ignore passive inputs)
if (action_id != 'mouse_move' && !action_id.startsWith('mouse_pos') && evt.pressed) {
if (action_id != 'mouse_move' && !starts_with(action_id, 'mouse_pos') && evt.pressed) {
var new_device = detect_device(action_id)
// For keyboard/mouse detection, also check if the event has modifier keys or is a mouse button
// This helps distinguish real keyboard/mouse events from mapped actions
var is_real_kb_mouse = (new_device == 'keyboard' &&
(evt.ctrl != null || evt.shift != null || evt.alt != null ||
action_id.startsWith('mouse_button')))
starts_with(action_id, 'mouse_button')))
if (new_device == 'keyboard' && !is_real_kb_mouse) {
// This might be a mapped action, not a real keyboard/mouse event
@@ -225,7 +225,7 @@ action.on_input = function(action_id, evt)
// 3) Otherwise, find all mapped actions for this input
var matched_actions = []
for (var mapped_action in this.action_map) {
if (this.action_map[mapped_action].includes(action_id)) {
if (find(this.action_map[mapped_action], action_id) != null) {
matched_actions.push(mapped_action)
if (evt.pressed)
@@ -254,8 +254,8 @@ action.rebind_action = function(action_name, new_key) {
// Remove this key from all other actions
for (var act in this.action_map) {
var idx = this.action_map[act].indexOf(new_key)
if (idx >= 0)
var idx = find(this.action_map[act], new_key)
if (idx != null)
this.action_map[act].splice(idx, 1)
}
@@ -275,7 +275,7 @@ action.rebind_action = function(action_name, new_key) {
action.get_bindings_for_device = function(action_name) {
var all = this.action_map[action_name] || []
var self = this
return all.filter(function(id) { return detect_device(id) == self.current_device })
return filter(all, function(id) { return detect_device(id) == self.current_device })
}
// Returns the primary binding for display - prefer keyboard/mouse, then others
@@ -284,7 +284,7 @@ action.get_primary_binding = function(action_name) {
if (!all.length) return '(unbound)'
// Prefer keyboard/mouse bindings for display stability (mouse buttons now detect as keyboard)
var keyboard = all.filter(function(id) { return detect_device(id) == 'keyboard' })
var keyboard = filter(all, function(id) { return detect_device(id) == 'keyboard' })
if (keyboard.length) return keyboard[0]
// Fall back to any binding
@@ -328,7 +328,7 @@ action.load_bindings = function() {
action.reset_to_defaults = function() {
for (var key in default_action_map) {
this.action_map[key] = default_action_map[key].slice()
this.action_map[key] = array(default_action_map[key])
}
this.save_bindings()
}
@@ -344,7 +344,7 @@ return function()
obj.current_gamepad_type = null
// Copy defaults
for (var key in default_action_map) {
obj.action_map[key] = default_action_map[key].slice()
obj.action_map[key] = array(default_action_map[key])
}
obj.load_bindings()
return obj

12
clay.cm
View File

@@ -116,12 +116,12 @@ function build_drawables(node, root_height, parent_abs_x, parent_abs_y, parent_s
// Intersect with parent
if (parent_scissor) {
sx = number.max(sx, parent_scissor.x)
sy = number.max(sy, parent_scissor.y)
var right = number.min(vis_x + sw, parent_scissor.x + parent_scissor.width)
var bottom = number.min(vis_y + sh, parent_scissor.y + parent_scissor.height)
sw = number.max(0, right - sx)
sh = number.max(0, bottom - sy)
sx = max(sx, parent_scissor.x)
sy = max(sy, parent_scissor.y)
var right = min(vis_x + sw, parent_scissor.x + parent_scissor.width)
var bottom = min(vis_y + sh, parent_scissor.y + parent_scissor.height)
sw = max(0, right - sx)
sh = max(0, bottom - sy)
}
current_scissor = {x: sx, y: sy, width: sw, height: sh}

View File

@@ -1,7 +1,7 @@
function tohex(n) {
var s = number.floor(n).toString(16);
var s = floor(n).toString(16);
if (s.length == 1) s = "0" + s;
return s.toUpperCase();
return upper(s);
};
var Color = {
@@ -22,19 +22,15 @@ Color.editor = {};
Color.editor.ur = Color.green;
Color.tohtml = function (v) {
var html = v.map(function (n) {
return tohex(n * 255);
});
return "#" + html.join("");
var html = array(v, n => tohex(n * 255));
return "#" + text(html);
};
var esc = {};
esc.reset = "\x1b[0";
esc.color = function (v) {
var c = v.map(function (n) {
return number.floor(n * 255);
});
var truecolor = "\x1b[38;2;" + c.join(";") + ";";
var c = array(v, n => floor(n * 255));
var truecolor = "\x1b[38;2;" + text(c, ";") + ";";
return truecolor;
};
@@ -92,7 +88,7 @@ Color.Editor = {
/* Detects the format of all colors and munges them into a floating point format */
Color.normalize = function (c) {
var add_a = function (a) {
var n = this.slice();
var n = array(this);
n[3] = a;
return n;
};
@@ -120,9 +116,7 @@ Color.normalize = function (c) {
// Convert from 0-255 to 0-1 if needed
if (needs_conversion) {
c[p] = c[p].map(function (x) {
return x / 255;
});
c[p] = array(c[p], x => x / 255);
}
c[p].alpha = add_a;
@@ -192,7 +186,7 @@ ColorMap.sample = function (t, map = this) {
if (t > 1) return map[1];
var lastkey = 0;
for (var key of array(map).sort()) {
for (var key of sorted(array(map))) {
if (t < key) {
var b = map[key];
var a = map[lastkey];

View File

@@ -353,16 +353,16 @@ function _calc_presentation(src, dst, mode) {
return {x: 0, y: 0, width: dst.width, height: dst.height}
if (mode == 'integer_scale') {
var sx = number.floor(dst.width / src.width)
var sy = number.floor(dst.height / src.height)
var s = number.max(1, number.min(sx, sy))
var sx = floor(dst.width / src.width)
var sy = floor(dst.height / src.height)
var s = max(1, min(sx, sy))
var w = src.width * s
var h = src.height * s
return {x: (dst.width - w) / 2, y: (dst.height - h) / 2, width: w, height: h}
}
// letterbox
var scale = number.min(dst.width / src.width, dst.height / src.height)
var scale = min(dst.width / src.width, dst.height / src.height)
var w = src.width * scale
var h = src.height * scale
return {x: (dst.width - w) / 2, y: (dst.height - h) / 2, width: w, height: h}

View File

@@ -6,7 +6,7 @@ var downkeys = {};
function keyname(key)
{
var str = input.keyname(key);
return str.toLowerCase();
return lower(str);
}
function modstr(mod = input.keymod()) {
@@ -66,7 +66,7 @@ prosperon.on('mouse_button_up', function(e) {
input.mouse = {};
input.mouse.screenpos = function mouse_screenpos() {
return mousepos.slice();
return array(mousepos);
};
input.mouse.worldpos = function mouse_worldpos() {
return prosperon.camera.screen2world(mousepos);
@@ -75,7 +75,7 @@ input.mouse.viewpos = function mouse_viewpos()
{
var world = input.mouse.worldpos();
return mousepos.slice();
return array(world)
}
input.mouse.disabled = function mouse_disabled() {
input.mouse_mode(1);
@@ -109,7 +109,7 @@ input.mouse.normal.doc = "Set the mouse to show again after hiding.";
input.keyboard = {};
input.keyboard.down = function (code) {
if (is_number(code)) return downkeys[code];
if (is_text(code)) return downkeys[code.toUpperCase().charCodeAt()] || downkeys[code.toLowerCase().charCodeAt()];
if (is_text(code)) return downkeys[codepoint(upper(code))] || downkeys[codepoint(lower(code))];
return null;
};
@@ -175,7 +175,7 @@ input.action = {
input.tabcomplete = function tabcomplete(val, list) {
if (!val) return val;
list = filter(x => x.startsWith(val))
list = filter(list, x => starts_with(x, val))
if (list.length == 1) {
return list[0];
@@ -186,14 +186,12 @@ input.tabcomplete = function tabcomplete(val, list) {
while (!ret && list.length != 0) {
var char = list[0][i];
if (
!list.every(function (x) {
return x[i] == char;
})
!every(list, x => x[i] == char)
)
ret = list[0].slice(0, i);
ret = text(list[0], 0, i);
else {
i++;
list = list.filter(x => x.length-1 > i)
list = filter(list, x => x.length-1 > i)
}
}
@@ -209,13 +207,13 @@ input.tabcomplete = function tabcomplete(val, list) {
var Player = {
players: [],
input(fn, ...args) {
this.pawns.forEach(x => x[fn]?.(...args));
arrfor(this.pawns, x => x[fn]?.(...args));
},
mouse_input(type, ...args) {
for (var pawn of [...this.pawns].reverse()) {
for (var pawn of reverse([...this.pawns])) {
if (is_function(pawn.inputs?.mouse?.[type])) {
pawn.inputs.mouse[type].call(pawn, ...args);
call(pawn.inputs.mouse[type], pawn, ...args);
pawn.inputs.post?.call(pawn);
if (!pawn.inputs.fallthru) return;
}
@@ -223,9 +221,9 @@ var Player = {
},
char_input(c) {
for (var pawn of [...this.pawns].reverse()) {
for (var pawn of reverse([...this.pawns])) {
if (is_function(pawn.inputs?.char)) {
pawn.inputs.char.call(pawn, c);
call(pawn.inputs.char, pawn, c);
pawn.inputs.post?.call(pawn);
if (!pawn.inputs.fallthru) return;
}
@@ -233,7 +231,7 @@ var Player = {
},
joy_input(name, joystick) {
for (var pawn of [...this.pawns].reverse()) {
for (var pawn of reverse([...this.pawns])) {
if (!pawn.inputs) return;
if (!pawn.inputs.joystick) return;
if (!pawn.inputs.joystick[name]) return;
@@ -250,7 +248,7 @@ var Player = {
},
raw_input(cmd, state, ...args) {
for (var pawn of [...this.pawns].reverse()) {
for (var pawn of reverse([...this.pawns])) {
var inputs = pawn.inputs;
if (!inputs[cmd]) {
@@ -277,10 +275,10 @@ var Player = {
var consumed = false;
if (is_function(fn)) {
fn.call(pawn, ...args);
call(fn, pawn, ...args);
consumed = true;
}
if (state == "released") inputs.release_post?.call(pawn);
if (state == "released") call(inputs.release_post, pawn);
if (inputs.block) return;
if (consumed) return;
}
@@ -295,12 +293,12 @@ var Player = {
},
print_pawns() {
[...this.pawns].reverse().forEach(x => log.console(x))
arrfor(reverse([...this.pawns]), x => log.console(x))
},
create() {
var n = meme(this);
n.pawns = new Set()
n.pawns = {}
n.gamepads = [];
this.players.push(n);
this[this.players.length - 1] = n;
@@ -314,18 +312,18 @@ var Player = {
if (!pawn.inputs)
throw Error("attempted to control a pawn without any input object.");
this.pawns.add(pawn);
this.pawns[pawn] = true
},
uncontrol(pawn) {
this.pawns.delete(pawn)
delete this.pawns[pawn]
},
};
input.do_uncontrol = function input_do_uncontrol(pawn) {
if (!pawn.inputs) return;
Player.players.forEach(function (p) {
p.pawns.delete(pawn)
arrfor(Player.players, function (p) {
delete p.pawns[pawn]
});
};

View File

@@ -97,6 +97,9 @@ function _main_loop() {
var win_size = _backend.get_window_size()
// Get input module for auto-ingestion
var input_mod = use('input')
for (var ev of evts) {
if (_config.imgui || _config.editor) {
imgui.process_event(ev)
@@ -117,6 +120,10 @@ function _main_loop() {
}
}
// Auto-ingest through input system
input_mod.ingest(ev_obj)
// Optional raw hook for debugging
if (_config.input) {
_config.input(ev_obj)
}

View File

@@ -143,7 +143,7 @@ function _render_node_summary(imgui, node) {
var info = []
if (node.pos) {
info.push("pos:(" + text(number.round(node.pos.x)) + "," + text(number.round(node.pos.y)) + ")")
info.push("pos:(" + text(round(node.pos.x)) + "," + text(round(node.pos.y)) + ")")
}
if (node.width && node.height) {
@@ -156,7 +156,7 @@ function _render_node_summary(imgui, node) {
if (node.text) {
var t = node.text
if (t.length > 20) t = t.substring(0, 17) + "..."
if (t.length > 20) t = text(t, 0, 17) + "..."
info.push("\"" + t + "\"")
}
@@ -165,11 +165,11 @@ function _render_node_summary(imgui, node) {
for (var i = 0; i < node.effects.length; i++) {
fx.push(node.effects[i].type)
}
info.push("fx:[" + fx.join(",") + "]")
info.push("fx:[" + text(fx, ",") + "]")
}
if (info.length > 0) {
imgui.text(" " + info.join(" "))
imgui.text(" " + text(info, " "))
}
}
@@ -221,10 +221,10 @@ function _render_node_inspector(imgui, node) {
// World transform (read-only)
if (node.world_pos) {
imgui.text("---")
imgui.text("World Position: (" + text(number.round(node.world_pos.x * 100) / 100) + ", " + text(number.round(node.world_pos.y * 100) / 100) + ")")
imgui.text("World Position: (" + text(round(node.world_pos.x * 100) / 100) + ", " + text(round(node.world_pos.y * 100) / 100) + ")")
}
if (node.world_opacity != null) {
imgui.text("World Opacity: " + text(number.round(node.world_opacity * 100) / 100))
imgui.text("World Opacity: " + text(round(node.world_opacity * 100) / 100))
}
// Image
@@ -360,8 +360,8 @@ function _render_pass_inspector(imgui, pass) {
imgui.text("Uniforms:")
for (var k in pass.uniforms) {
var v = pass.uniforms[k]
if (Array.isArray(v)) {
imgui.text(" " + k + ": [" + v.join(", ") + "]")
if (is_array(v)) {
imgui.text(" " + k + ": [" + text(v, ", ") + "]")
} else {
imgui.text(" " + k + ": " + text(v))
}
@@ -372,10 +372,10 @@ function _render_pass_inspector(imgui, pass) {
if (pass.color) {
imgui.text("---")
imgui.text("Clear: rgba(" +
text(number.round(pass.color.r * 255)) + "," +
text(number.round(pass.color.g * 255)) + "," +
text(number.round(pass.color.b * 255)) + "," +
text(number.round(pass.color.a * 100) / 100) + ")")
text(round(pass.color.r * 255)) + "," +
text(round(pass.color.g * 255)) + "," +
text(round(pass.color.b * 255)) + "," +
text(round(pass.color.a * 100) / 100) + ")")
}
// Source size
@@ -449,8 +449,8 @@ function _render_stats(imgui, stats) {
if (stats.fps) {
imgui.text("---")
imgui.text("FPS: " + text(number.round(stats.fps)))
imgui.text("Frame time: " + text(number.round(stats.frame_time_ms * 100) / 100) + " ms")
imgui.text("FPS: " + text(round(stats.fps)))
imgui.text("Frame time: " + text(round(stats.frame_time_ms * 100) / 100) + " ms")
}
})
}

View File

@@ -130,7 +130,7 @@ Ease.zoom = {
return function(t) {
if (t == 0) return 0
if (t == 1) return 1
if (number.abs(ratio - 1) < 0.001) return t
if (abs(ratio - 1) < 0.001) return t
// Position interpolation formula: (r^t - 1) / (r - 1)
return (math.power(ratio, t) - 1) / (ratio - 1)
}

View File

@@ -15,7 +15,7 @@ action.on_input = function(action_id, action_data)
{
if (!action_data.pressed) return
if (!valid_keys.includes(action_id))
if (find(valid_keys, action_id) == null)
return
// Only process key events with modifiers or if we're waiting for a prefix continuation
@@ -35,7 +35,7 @@ action.on_input = function(action_id, action_data)
var key = action_id
if (key.length == 1) {
// Single character keys
emacs_notation += key.toLowerCase()
emacs_notation += lower(key)
} else {
// Handle special keys
switch (key) {

View File

@@ -72,7 +72,7 @@ function handleMouseButtonDown(e) {
var mx = e.mouse.x;
var my = e.mouse.y;
var c = [number.floor(mx / 60), number.floor(my / 60)];
var c = [floor(mx / 60), floor(my / 60)];
if (!grid.inBounds(c)) return;
var cell = grid.at(c);
@@ -103,7 +103,7 @@ function handleMouseButtonUp(e) {
var mx = e.mouse.x;
var my = e.mouse.y;
var c = [number.floor(mx / 60), number.floor(my / 60)];
var c = [floor(mx / 60), floor(my / 60)];
if (!grid.inBounds(c)) {
holdingPiece = false;
return;
@@ -138,7 +138,7 @@ function handleMouseMotion(e) {
var mx = e.pos.x;
var my = e.pos.y;
var c = [number.floor(mx / 60), number.floor(my / 60)];
var c = [floor(mx / 60), floor(my / 60)];
if (!grid.inBounds(c)) {
hoverPos = null;
return;

View File

@@ -25,7 +25,7 @@ grid.prototype = {
// add an entity into a cell
add(entity, pos) {
this.cell(pos.x, pos.y).push(entity);
entity.coord = pos.slice();
entity.coord = array(pos);
},
// remove an entity from a cell

View File

@@ -7,7 +7,7 @@ function Piece(kind, colour) {
this.coord = [0,0];
}
Piece.prototype.toString = function () {
return this.colour.charAt(0) + this.kind.charAt(0).toUpperCase();
return character(this.colour) + upper(character(this.kind));
};
function startingPosition(grid) {

View File

@@ -11,20 +11,20 @@ var deltas = {
var two = (dy == 2 * dir && dx == 0 && cy(pc.coord) == base &&
grid.at({ x: cx(pc.coord), y: cy(pc.coord)+dir }).length == 0 &&
grid.at(to).length == 0);
var cap = (dy == dir && number.abs(dx) == 1 && grid.at(to).length);
var cap = (dy == dir && Math.abs(dx) == 1 && grid.at(to).length);
return one || two || cap;
},
rook : function (pc, dx, dy) { return (dx == 0 || dy == 0); },
bishop: function (pc, dx, dy) { return number.abs(dx) == number.abs(dy); },
queen : function (pc, dx, dy) { return (dx == 0 || dy == 0 || number.abs(dx) == number.abs(dy)); },
knight: function (pc, dx, dy) { return (number.abs(dx) == 1 && number.abs(dy) == 2) ||
(number.abs(dx) == 2 && number.abs(dy) == 1); },
king : function (pc, dx, dy) { return number.max(number.abs(dx), number.abs(dy)) == 1; }
bishop: function (pc, dx, dy) { return Math.abs(dx) == Math.abs(dy); },
queen : function (pc, dx, dy) { return (dx == 0 || dy == 0 || Math.abs(dx) == Math.abs(dy)); },
knight: function (pc, dx, dy) { return (Math.abs(dx) == 1 && Math.abs(dy) == 2) ||
(Math.abs(dx) == 2 && Math.abs(dy) == 1); },
king : function (pc, dx, dy) { return Math.max(Math.abs(dx), Math.abs(dy)) == 1; }
};
function clearLine(from, to, grid) {
var dx = number.sign(cx(to) - cx(from));
var dy = number.sign(cy(to) - cy(from));
var dx = Math.sign(cx(to) - cx(from));
var dy = Math.sign(cy(to) - cy(from));
var x = cx(from) + dx, y = cy(from) + dy;
while (x != cx(to) || y != cy(to)) {
if (grid.at({ x: x, y: y }).length) return false;

View File

@@ -60,10 +60,10 @@ this.update = function(dt) {
// Collide with paddle 1?
if (r>left1 && l<right1 && t>bottom1 && b<top1)
ball.vx = number.abs(ball.vx)
ball.vx = abs(ball.vx)
// Collide with paddle 2?
if (r>left2 && l<right2 && t>bottom2 && b<top2)
ball.vx = -number.abs(ball.vx)
ball.vx = -abs(ball.vx)
// Check left/right out-of-bounds
if (r<0) { score2++; resetBall() }

View File

@@ -10,8 +10,8 @@ var random = use('random')
prosperon.camera.transform.pos = [0,0]
var cellSize = 20
var gridW = number.floor(config.width / cellSize)
var gridH = number.floor(config.height / cellSize)
var gridW = floor(config.width / cellSize)
var gridH = floor(config.height / cellSize)
var snake, direction, nextDirection, apple
var moveInterval = 0.1
@@ -19,8 +19,8 @@ var moveTimer = 0
var gameState = "playing"
function resetGame() {
var cx = number.floor(gridW / 2)
var cy = number.floor(gridH / 2)
var cx = floor(gridW / 2)
var cy = floor(gridH / 2)
snake = [
{x: cx, y: cy},
{x: cx-1, y: cy},
@@ -34,7 +34,7 @@ function resetGame() {
}
function spawnApple() {
apple = {x:number.floor(random.random()*gridW), y:number.floor(random.random()*gridH)}
apple = {x:floor(random.random()*gridW), y:floor(random.random()*gridH)}
// Re-spawn if apple lands on snake
for (var i=0; i<snake.length; i++)
if (snake[i].x == apple.x && snake[i].y == apple.y) { spawnApple(); return }

View File

@@ -65,12 +65,12 @@ function initBoard() {
initBoard()
function randomShape() {
var key = shapeKeys[number.floor(random.random()*shapeKeys.length)]
var key = shapeKeys[floor(random.random()*shapeKeys.length)]
// Make a copy of the shapes blocks
return {
type: key,
color: SHAPES[key].color,
blocks: SHAPES[key].blocks.map(b => [b[0], b[1]])
blocks: array(SHAPES[key].blocks, b => [b[0], b[1]])
}
}
@@ -116,7 +116,7 @@ function rotate(blocks) {
function clearLines() {
var lines = 0
for (var r=ROWS-1; r>=0;) {
if (board[r].every(cell => cell)) {
if (every(board[r], cell => cell)) {
lines++
// remove row
board.splice(r,1)
@@ -134,7 +134,7 @@ function clearLines() {
else if (lines==3) score += 500
else if (lines==4) score += 800
linesCleared += lines
level = number.floor(linesCleared/10)
level = floor(linesCleared/10)
}
function placePiece() {
@@ -198,7 +198,7 @@ this.update = function(dt) {
if (input.keyboard.down('w')) {
if (!rotateHeld) {
rotateHeld = true
var test = piece.blocks.map(b => [b[0], b[1]])
var test = array(piece.blocks, b => [b[0], b[1]])
rotate(test)
if (!collides(pieceX, pieceY, test)) piece.blocks = test
}
@@ -211,7 +211,7 @@ this.update = function(dt) {
// Gravity
gravityTimer += dt * fallSpeed
var dropInterval = number.max(0.1, baseGravity - level*0.05)
var dropInterval = max(0.1, baseGravity - level*0.05)
if (gravityTimer >= dropInterval) {
gravityTimer = 0
if (!collides(pieceX, pieceY+1, piece.blocks)) {

203
film2d.cm
View File

@@ -1,10 +1,115 @@
var film2d = {}
var backend = null
film2d.set_backend = function(b) {
backend = b
}
var next_id = 1
var registry = {} // id -> drawable
var group_index = {} // group_name -> [id, id, ...]
var plane_index = {} // plane_name -> [id, id, ...]
// Resolve sprite dimensions and UV based on fit mode
// fit: 'fill' | 'contain' | 'cover' | 'none'
// - fill: stretch to exactly match box (distort)
// - contain: fit inside box, preserve aspect (letterbox)
// - cover: fill box, preserve aspect (crop)
// - none: use native pixel size (or pixels_per_tile driven)
function _resolve_sprite_fit(sprite) {
if (!backend || !backend.get_texture_info) return sprite
var img = sprite.texture || sprite.image
if (!img) return sprite
var tex_info = backend.get_texture_info(img)
if (!tex_info || !tex_info.width || !tex_info.height) return sprite
var tex_w = tex_info.width
var tex_h = tex_info.height
var tex_aspect = tex_w / tex_h
var target_w = sprite.width
var target_h = sprite.height
var fit = sprite.fit || 'none'
// If one dimension is null, derive from aspect ratio
if (target_w == null && target_h != null) {
target_w = target_h * tex_aspect
sprite.width = target_w
sprite.height = target_h
return sprite
}
if (target_h == null && target_w != null) {
target_h = target_w / tex_aspect
sprite.width = target_w
sprite.height = target_h
return sprite
}
// Both null - use native size (1 pixel = 1 unit, or could use pixels_per_tile)
if (target_w == null && target_h == null) {
sprite.width = tex_w
sprite.height = tex_h
return sprite
}
// Both dimensions specified - apply fit mode
var box_aspect = target_w / target_h
if (fit == 'fill') {
// Stretch to fill - no changes needed, just use target dimensions
sprite.width = target_w
sprite.height = target_h
return sprite
}
if (fit == 'contain') {
// Fit inside box, preserve aspect (letterbox)
var scale
if (tex_aspect > box_aspect) {
// Image wider than box - constrain by width
scale = target_w / tex_w
} else {
// Image taller than box - constrain by height
scale = target_h / tex_h
}
sprite.width = tex_w * scale
sprite.height = tex_h * scale
return sprite
}
if (fit == 'cover') {
// Fill box, preserve aspect (crop via UV)
var fit_ax = sprite.fit_anchor_x != null ? sprite.fit_anchor_x : 0.5
var fit_ay = sprite.fit_anchor_y != null ? sprite.fit_anchor_y : 0.5
var scale_w = target_w / tex_w
var scale_h = target_h / tex_h
var scale = max(scale_w, scale_h)
// Compute visible portion of texture in UV space
var visible_w = target_w / scale
var visible_h = target_h / scale
// UV rect (0-1 space)
var uv_w = visible_w / tex_w
var uv_h = visible_h / tex_h
var uv_x = (1 - uv_w) * fit_ax
var uv_y = (1 - uv_h) * fit_ay
sprite.width = target_w
sprite.height = target_h
sprite.uv_rect = {x: uv_x, y: uv_y, width: uv_w, height: uv_h}
return sprite
}
// fit == 'none' - use native size
sprite.width = tex_w
sprite.height = tex_h
return sprite
}
film2d.register = function(drawable) {
var id = text(next_id++)
drawable._id = id
@@ -34,8 +139,8 @@ film2d.unregister = function(id) {
// Remove from plane index
var plane = drawable.plane || 'default'
if (plane_index[plane]) {
var idx = plane_index[plane].indexOf(id_str)
if (idx >= 0) plane_index[plane].splice(idx, 1)
var idx = find(plane_index[plane], id_str)
if (idx != null) plane_index[plane].splice(idx, 1)
}
// Remove from group indices
@@ -43,8 +148,8 @@ film2d.unregister = function(id) {
for (var i = 0; i < groups.length; i++) {
var g = groups[i]
if (group_index[g]) {
var idx = group_index[g].indexOf(id_str)
if (idx >= 0) group_index[g].splice(idx, 1)
var idx = find(group_index[g], id_str)
if (idx != null) group_index[g].splice(idx, 1)
}
}
@@ -144,13 +249,15 @@ film2d.all_groups = function() {
}
// Render function - takes drawables directly, no tree traversal
film2d.render = function(params, backend) {
film2d.render = function(params, render_backend) {
backend = render_backend
var drawables = params.drawables || []
var camera = params.camera
var target = params.target
var target_size = params.target_size
var clear_color = params.clear
var layer_sort = params.layer_sort || {} // layer -> 'y' or 'explicit'
var layer_sort = params.layer_sort || {} // layer(text) -> "y" or "explicit"
if (drawables.length == 0) return { commands: [] }
@@ -160,56 +267,71 @@ film2d.render = function(params, backend) {
var h = d.height || 0
var ay = d.anchor_y
if (ay == null) ay = 0.5
// Convert "pos.y at anchor" -> "feet y"
return y + h * (1 - ay)
return y + h * (1 - ay) // "pos at anchor" -> "feet y"
}
// Sort by layer, then optionally by Y based on layer_sort policy
drawables.sort(function(a, b) {
var al = a.layer || 0
var bl = b.layer || 0
var dl = al - bl
if (dl != 0) return dl
// Deterministic "explicit" order anchor (keep if you want _id tie-break behavior)
drawables = sort(drawables, "_id")
var sort_mode = layer_sort[text(al)] || 'explicit'
if (sort_mode == 'y') {
var ay = _y_sort_key(a)
var by = _y_sort_key(b)
// Bucket drawables by layer
var buckets = Object.create(null) // layer_text -> array
for (var i = 0; i < drawables.length; i++) {
var d = drawables[i]
var layer_key = String(d.layer || 0)
var b = buckets[layer_key]
if (!b) {
b = []
buckets[layer_key] = b
}
b.push(d)
}
// Make this explicit instead of guessing
// Sort layers numerically (keys are text)
var layers = array(buckets) // text keys
var layer_nums = array(layers, k => number(k))
layers = sort(layers, layer_nums)
// Merge buckets, y-sorting buckets that request it
var y_down = camera && camera.y_down == true
var sorted_drawables = []
// If y_down: bigger y is lower on screen => should draw later (on top)
// If y_up: smaller y is lower on screen => should draw later (on top)
if (ay != by) return y_down ? (ay - by) : (by - ay)
for (var li = 0; li < layers.length; li++) {
var layer_key = layers[li]
var b = buckets[layer_key]
var mode = layer_sort[layer_key] || "explicit"
if (mode == "y") {
var keys = array(b, d => _y_sort_key(d))
b = sort(b, keys) // ascending feet-y
if (!y_down) b = reverse(b) // y_up => smaller y draws later => reverse
}
var aid = a._id || 0
var bid = b._id || 0
return aid < bid ? -1 : 1
})
for (var j = 0; j < b.length; j++) sorted_drawables.push(b[j])
}
drawables = sorted_drawables
var commands = []
commands.push({cmd: 'begin_render', target: target, clear: clear_color})
commands.push({cmd: 'set_camera', camera: camera})
commands.push({ cmd: "begin_render", target: target, clear: clear_color, target_size: target_size })
commands.push({ cmd: "set_camera", camera: camera })
var batches = _batch_drawables(drawables)
for (var i = 0; i < batches.length; i++) {
var batch = batches[i]
if (batch.type == 'sprite_batch')
commands.push({cmd: 'draw_batch', batch_type: 'sprites', geometry: {sprites: batch.sprites}, texture: batch.texture, material: batch.material})
else if (batch.type == 'mesh2d_batch')
commands.push({cmd: 'draw_mesh2d', meshes: batch.meshes, texture: batch.texture, material: batch.material})
else if (batch.type == 'text')
commands.push({cmd: 'draw_text', drawable: batch.drawable})
else if (batch.type == 'texture_ref')
commands.push({cmd: 'draw_texture_ref', drawable: batch.drawable})
else if (batch.type == 'shape')
commands.push({cmd: 'draw_shape', drawable: batch.drawable})
if (batch.type == "sprite_batch")
commands.push({ cmd: "draw_batch", batch_type: "sprites", geometry: { sprites: batch.sprites }, texture: batch.texture, material: batch.material })
else if (batch.type == "mesh2d_batch")
commands.push({ cmd: "draw_mesh2d", meshes: batch.meshes, texture: batch.texture, material: batch.material })
else if (batch.type == "text")
commands.push({ cmd: "draw_text", drawable: batch.drawable })
else if (batch.type == "texture_ref")
commands.push({ cmd: "draw_texture_ref", drawable: batch.drawable })
else if (batch.type == "shape")
commands.push({ cmd: "draw_shape", drawable: batch.drawable })
}
commands.push({cmd: 'end_render'})
commands.push({ cmd: "end_render" })
return { commands: commands }
}
@@ -222,6 +344,9 @@ function _batch_drawables(drawables) {
var d = drawables[i]
if (d.type == 'sprite') {
// Resolve fit mode (computes final width/height/uv_rect)
_resolve_sprite_fit(d)
var tex = d.texture || d.image
var mat = d.material || {blend: 'alpha', sampler: d.filter || 'nearest'}

View File

@@ -255,7 +255,7 @@ NODE_EXECUTORS.clip_rect = function(params, backend) {
if (!input) return {target: null, commands: []}
// Clip doesn't need a new target, just adds scissor to commands
var commands = input.commands ? input.commands.slice() : []
var commands = input.commands ? array(input.commands) : []
// Insert scissor after begin_render
var insert_idx = 0

View File

@@ -16,7 +16,7 @@ gesture.reset = function() {
}
gesture.on_input = function(action_id, action) {
if (!action_id.includes('gamepad_touchpad_')) return
if (search(action_id, 'gamepad_touchpad_') == null) return
var finger = action.finger || 0
var touchpad = action.touchpad || 0
@@ -59,7 +59,7 @@ gesture.on_input = function(action_id, action) {
var currentDist = this.dist(fingers[0], fingers[1])
var d = currentDist - this.startDist
if (number.abs(d) >= this.PINCH_TH) {
if (abs(d) >= this.PINCH_TH) {
var gesture_type = d > 0 ? 'pinch_out' : 'pinch_in'
// scene.recurse(game.root, 'on_input', gesture_type, { delta: d })
this.startDist = currentDist
@@ -79,7 +79,7 @@ gesture.on_input = function(action_id, action) {
var dy = action.y - touch.startY
if (dt < this.MAX_TIME / 1000) { // Convert to seconds
var absX = number.abs(dx), absY = number.abs(dy)
var absX = abs(dx), absY = abs(dy)
if (absX > this.MIN_SWIPE / 100 || absY > this.MIN_SWIPE / 100) { // Normalize for 0-1 range
var dir = absX > absY
? (dx > 0 ? 'swipe_right' : 'swipe_left')

View File

@@ -55,10 +55,10 @@ function decorate_rect_px(img) {
// store pixel-space version: [x, y, w, h] in texels
img.rect_px = {
x:number.round(img.rect.x * width),
y:number.round(img.rect.y * height),
width:number.round(img.rect.width * width),
height:number.round(img.rect.height * height)
x:round(img.rect.x * width),
y:round(img.rect.y * height),
width:round(img.rect.width * width),
height:round(img.rect.height * height)
}
}
@@ -75,7 +75,7 @@ function wrapSurface(surf, maybeRect){
return h;
}
function wrapFrames(arr){ /* [{surface,time,rect}, …] → [{image,time}] */
return arr.map(f => {
return array(arr, f => {
// Handle both surface objects and objects with surface property
var surf = f.surface || f;
return {
@@ -119,7 +119,7 @@ function decode_gif(decoded) {
}
// Multiple frames - return array with time property
return decoded.frames.map(function(frame) {
return array(decoded.frames, frame => {
return {
surface: frame,
time: (frame.duration || 100) / 1000.0 // convert ms to seconds
@@ -138,7 +138,7 @@ function decode_aseprite(decoded) {
// Multiple frames without tags - return as single animation
return {
frames: decoded.frames.map(function(frame) {
frames: array(decoded.frames, frame => {
return {
surface: frame,
time: (frame.duration || 100) / 1000.0 // convert ms to seconds
@@ -152,7 +152,7 @@ function create_image(path){
try{
def bytes = io.slurp(path);
var ext = path.split('.').pop()
var ext = array(path, '.').pop()
var raw = decode_image(bytes, ext);
/* ── Case A: single surface (from make_texture) ────────────── */
@@ -258,7 +258,7 @@ graphics.texture = function texture(path) {
if (!is_text(path))
throw Error('need a string for graphics.texture')
var parts = path.split(':')
var parts = array(path, ':')
var id = parts[0]
var animName = parts[1]
var frameIndex = parts[2]
@@ -353,7 +353,7 @@ graphics.texture = function texture(path) {
}
graphics.tex_hotreload = function tex_hotreload(file) {
var basename = file.split('/').pop().split('.')[0]
var basename = array(array(file, '/').pop(), '.')[0]
// Check if this basename exists in our cache
if (!(basename in cache)) return
@@ -390,7 +390,7 @@ graphics.tex_hotreload = function tex_hotreload(file) {
Merges specific properties from nv into ov, using an array of property names.
*/
function merge_objects(ov, nv, arr) {
arr.forEach(x => ov[x] = nv[x])
arrfor(arr, x => ov[x] = nv[x])
}
/**
@@ -411,7 +411,7 @@ graphics.get_font = function get_font(path) {
if (!is_text(path))
throw Error(`Can't find font with path: ${path}`)
var parts = path.split('.')
var parts = array(path, '.')
var size = 16 // default size
parts[1] = number(parts[1])
if (parts[1]) {
@@ -434,7 +434,7 @@ graphics.get_font = function get_font(path) {
}
graphics.queue_sprite_mesh = function(queue) {
var sprites = queue.filter(x => x.type == 'sprite')
var sprites = filter(queue, x => x.type == 'sprite')
if (sprites.length == 0) return []
var mesh = graphics.make_sprite_mesh(sprites)
for (var i = 0; i < sprites.length; i++) {

278
input.cm
View File

@@ -1,12 +1,274 @@
var sdl_input = use('sdl3/input')
var emacs = use('emacs')
var gestures = use('gestures')
// Prosperon Input System
// Engine-driven input with user pairing and possession dispatch
var backend = use('input/backends/sdl3')
var devices = use('input/devices')
var bindings_mod = use('input/bindings')
var router_mod = use('input/router')
// Default UI-focused action map
var default_action_map = {
'ui_up': ['w', 'up', 'gamepad_dpup'],
'ui_down': ['s', 'down', 'gamepad_dpdown'],
'ui_left': ['a', 'left', 'gamepad_dpleft'],
'ui_right': ['d', 'right', 'gamepad_dpright'],
'confirm': ['return', 'space', 'mouse_button_left', 'gamepad_a'],
'cancel': ['escape', 'gamepad_b'],
'menu': ['escape', 'gamepad_start']
}
var default_display_names = {
'ui_up': 'UI Up',
'ui_down': 'UI Down',
'ui_left': 'UI Left',
'ui_right': 'UI Right',
'confirm': 'Confirm',
'cancel': 'Cancel',
'menu': 'Menu'
}
// Module state
var _users = []
var _config = {
max_users: 1,
pairing: 'last_used',
emacs: true,
gestures: true,
action_map: default_action_map,
display_names: default_display_names
}
var _initialized = false
var _window_callback = null
// Create an input user
function create_user(index, config) {
var action_map = {}
var display_names = {}
// Merge defaults with config
for (var k in default_action_map) {
action_map[k] = array(default_action_map[k])
display_names[k] = default_display_names[k]
}
if (config.action_map) {
for (var k in config.action_map) {
var val = config.action_map[k]
action_map[k] = is_array(val) ? array(val) : [val]
}
}
if (config.display_names) {
for (var k in config.display_names) {
display_names[k] = config.display_names[k]
}
}
var user = {
index: index,
paired_devices: [],
active_device: null,
bindings: bindings_mod.make(action_map, display_names),
router: null,
control_stack: [],
// Get current device kind
get device_kind() {
if (!this.active_device) return 'keyboard'
return devices.kind(this.active_device)
},
// Get current gamepad type
get gamepad_type() {
if (!this.active_device) return null
return devices.gamepad_type(this.active_device)
},
// Get action down state
get down() {
return this.router ? this.router.down : {}
},
// Possess an entity (clears stack, sets as sole target)
possess: function(entity) {
this.control_stack = [entity]
},
// Push entity onto control stack
push: function(entity) {
this.control_stack.push(entity)
},
// Pop from control stack
pop: function() {
if (this.control_stack.length > 1) {
return this.control_stack.pop()
}
return null
},
// Get current control target
get target() {
return this.control_stack.length > 0
? this.control_stack[this.control_stack.length - 1]
: null
},
// Dispatch action to current target
dispatch: function(action, data) {
var target = this.target
if (target && target.on_input) {
target.on_input(action, data)
}
},
// Get icon for action using current device
get_icon_for_action: function(action) {
return this.bindings.get_icon_for_action(action, this.device_kind, this.gamepad_type)
},
// Get primary binding for action using current device
get_primary_binding: function(action) {
return this.bindings.get_primary_binding(action, this.device_kind)
}
}
// Create router
user.router = router_mod.make(user, {
emacs: config.emacs,
gestures: config.gestures,
swipe_min_dist: config.swipe_min_dist,
swipe_max_time: config.swipe_max_time,
pinch_threshold: config.pinch_threshold
})
// Load saved bindings
user.bindings.load()
return user
}
// Pick user based on pairing policy
function pick_user(canon) {
if (_users.length == 0) return null
// For last_used: always user 0, just update active device
if (_config.pairing == 'last_used') {
var user = _users[0]
// Only switch on button press, not axis/motion
if (canon.kind == 'button' && canon.pressed) {
if (user.active_device != canon.device_id) {
// Release all held actions when switching device
var old_down = user.router.down
for (var action in old_down) {
if (old_down[action]) {
user.dispatch(action, { pressed: false, released: true, time: canon.time })
}
}
user.active_device = canon.device_id
if (find(user.paired_devices, canon.device_id) == null) {
user.paired_devices.push(canon.device_id)
}
}
}
return user
}
// For explicit pairing: find user paired to this device
for (var i = 0; i < _users.length; i++) {
if (find(_users[i].paired_devices, canon.device_id) != null) {
_users[i].active_device = canon.device_id
return _users[i]
}
}
// Unpaired device - could implement join logic here
return null
}
// Configure the input system
function configure(opts) {
opts = opts || {}
_config.max_users = opts.max_users || 1
_config.pairing = opts.pairing || 'last_used'
_config.emacs = opts.emacs != false
_config.gestures = opts.gestures != false
if (opts.action_map) _config.action_map = opts.action_map
if (opts.display_names) _config.display_names = opts.display_names
if (opts.on_window) _window_callback = opts.on_window
// Copy gesture config
_config.swipe_min_dist = opts.swipe_min_dist
_config.swipe_max_time = opts.swipe_max_time
_config.pinch_threshold = opts.pinch_threshold
// Create users
_users = []
for (var i = 0; i < _config.max_users; i++) {
_users.push(create_user(i, _config))
}
_initialized = true
}
// Ingest a raw SDL event
function ingest(raw_evt) {
if (!_initialized) {
configure({})
}
// Translate to canonical format
var canon = backend.translate(raw_evt)
if (!canon) return
// Handle window events specially
if (canon.kind == 'window') {
if (_window_callback) {
_window_callback(canon)
}
return
}
// Handle device events
if (canon.kind == 'device') {
if (canon.control == 'connected') {
devices.register(canon)
} else if (canon.control == 'disconnected') {
devices.unregister(canon.device_id)
}
return
}
// Register device
devices.register(canon)
// Pick user and route
var user = pick_user(canon)
if (user) {
user.router.handle(canon)
}
}
// Get user by index
function user(index) {
return _users[index]
}
return {
gamepad_id_to_tyle: sdl_input.gamepad_id_to_type,
make: function() {
return {
configure: configure,
ingest: ingest,
user: user,
}
}
get player1() { return _users[0] },
get player2() { return _users[1] },
get player3() { return _users[2] },
get player4() { return _users[3] },
// Re-export for convenience
devices: devices,
backend: backend
}

View File

@@ -43,30 +43,6 @@ var line_proto = {
return this
},
set_groups: function(groups) {
var old_groups = this.groups
this.groups = groups
film2d.reindex(this._id, old_groups, groups)
return this
},
add_group: function(group) {
if (this.groups.indexOf(group) < 0) {
this.groups.push(group)
film2d.index_group(this._id, group)
}
return this
},
remove_group: function(group) {
var idx = this.groups.indexOf(group)
if (idx >= 0) {
this.groups.splice(idx, 1)
film2d.unindex_group(this._id, group)
}
return this
},
_rebuild: function() {
var result = build_polyline_mesh(this)
this.verts = result.verts

View File

@@ -372,7 +372,7 @@ PlaydateBackend.prototype.gray_to_dither_pattern = function(gray) {
[0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF]
]
var index = number.floor(gray * (patterns.length - 1))
var index = floor(gray * (patterns.length - 1))
return patterns[index]
}

View File

@@ -26,10 +26,10 @@ rasterize.ellipse = function ellipse(pos, radii, opt) {
var cx = pos[0], cy = pos[1]
var raw_start = opt.start || 0
var raw_end = opt.end || 1
var full_circle = number.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 end = (raw_end % 1 + 1) % 1
var thickness = number.max(1, opt.thickness || 1)
var thickness = max(1, opt.thickness || 1)
var rx_i = rx - thickness,
ry_i = ry - thickness
@@ -46,8 +46,8 @@ rasterize.ellipse = function ellipse(pos, radii, opt) {
var pts = [
[cx + x, cy + y], [cx - x, cy + y],
[cx + x, cy - y], [cx - x, cy - y]
].filter(pt => within_wedge(pt[0]-cx, pt[1]-cy, start, end, full_circle))
points = points.concat(pts)
]
points = array(points, filter(pts, pt => within_wedge(pt[0]-cx, pt[1]-cy, start, end, full_circle)))
}
while (px < py) {
@@ -72,14 +72,14 @@ rasterize.ellipse = function ellipse(pos, radii, opt) {
for (var dy = -ry; dy <= ry; ++dy) {
var yy = dy * dy
var x_out = number.floor(rx * math.sqrt(1 - yy / ry_sq))
var x_out = floor(rx * math.sqrt(1 - yy / ry_sq))
var y_screen = cy + dy
var x_in = hole ? number.floor(rx_i * math.sqrt(1 - yy / ry_i_sq)) : -1
var x_in = hole ? floor(rx_i * math.sqrt(1 - yy / ry_i_sq)) : -1
var run_start = null
for (var dx = -x_out; dx <= x_out; ++dx) {
if (hole && number.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 (run_start == null) run_start = cx + dx
@@ -87,7 +87,7 @@ rasterize.ellipse = function ellipse(pos, radii, opt) {
var last = (dx == x_out)
var next_in_ring =
!last &&
!(hole && number.abs(dx+1) <= x_in) &&
!(hole && abs(dx+1) <= x_in) &&
within_wedge(dx+1, dy, start, end, full_circle)
if (last || !next_in_ring) {
@@ -140,7 +140,7 @@ rasterize.round_rect = function round_rect(rect, radius, thickness) {
return rasterize.fill_round_rect(rect, radius)
}
radius = number.min(radius, rect.width >> 1, rect.height >> 1)
radius = min(radius, rect.width >> 1, rect.height >> 1)
if ((thickness << 1) >= rect.width ||
(thickness << 1) >= rect.height ||
@@ -169,9 +169,9 @@ rasterize.round_rect = function round_rect(rect, radius, thickness) {
for (var dy = 0; dy < radius; ++dy) {
var dy_sq = dy * dy
var dx_out = number.floor(math.sqrt(r_out * r_out - dy_sq))
var dx_out = floor(math.sqrt(r_out * r_out - dy_sq))
var dx_in = (r_in > 0 && dy < r_in)
? number.floor(math.sqrt(r_in * r_in - dy_sq))
? floor(math.sqrt(r_in * r_in - dy_sq))
: -1
var w = dx_out - dx_in
if (w <= 0) continue
@@ -184,11 +184,11 @@ rasterize.round_rect = function round_rect(rect, radius, thickness) {
)
}
return {type: 'rects', data: rects.concat(strips)}
return {type: 'rects', data: array(rects, strips)}
}
rasterize.fill_round_rect = function fill_round_rect(rect, radius) {
radius = number.min(radius, rect.width >> 1, rect.height >> 1)
radius = min(radius, rect.width >> 1, rect.height >> 1)
var x0 = rect.x,
y0 = rect.y,
@@ -206,7 +206,7 @@ rasterize.fill_round_rect = function fill_round_rect(rect, radius) {
var caps = []
for (var dy = 0; dy < radius; ++dy) {
var dx = number.floor(math.sqrt(radius * radius - dy * dy))
var dx = floor(math.sqrt(radius * radius - dy * dy))
var w = (dx << 1) + 1
caps.push(
@@ -217,7 +217,7 @@ rasterize.fill_round_rect = function fill_round_rect(rect, radius) {
)
}
return {type: 'rects', data: rects.concat(caps)}
return {type: 'rects', data: array(rects, caps)}
}
return rasterize

View File

@@ -23,24 +23,24 @@ Resources.lib = [".so", ".dll", ".dylib"]
function getExtension(path) {
var idx = path.lastIndexOf('.')
if (idx < 0) return ''
return path.substring(idx + 1).toLowerCase()
return lower(text(path, idx + 1))
}
// Return true if ext is in at least one of the recognized lists
function isRecognizedExtension(ext) {
if (!ext) return false
if (Resources.scripts.includes(ext)) return true
if (Resources.images.includes(ext)) return true
if (Resources.sounds.includes(ext)) return true
if (Resources.fonts.includes(ext)) return true
if (Resources.lib.includes('.' + ext)) return true // for .so or .dll
if (search(Resources.scripts, ext) != null) return true
if (search(Resources.images, ext) != null) return true
if (search(Resources.sounds, ext) != null) return true
if (search(Resources.fonts, ext) != null) return true
if (search(Resources.lib, '.' + ext) != null) return true // for .so or .dll
return false
}
function find_in_path(filename, exts = []) {
if (!is_text(filename)) return null
if (filename.includes('.')) {
if (search(filename, '.') != null) {
var candidate = filename // possibly need "/" ?
if (io.exists(candidate) && !io.is_directory(candidate)) return candidate
return null
@@ -87,10 +87,10 @@ function read_ignore(dir) {
var path = dir + '/.prosperonignore'
var patterns = []
if (io.exists(path)) {
var lines = io.slurp(path).split('\n')
var lines = array(io.slurp(path), '\n')
for (var line of lines) {
line = line.trim()
if (!line || line.startsWith('#')) continue
line = trim(line)
if (!line || starts_with(line, '#')) continue
patterns.push(line)
}
}
@@ -124,19 +124,19 @@ Resources.gatherStats = function(filePaths) {
}
for (var path of filePaths) {
var ext = getExtension(path)
if (Resources.scripts.includes(ext)) {
if (find(Resources.scripts, ext) != null) {
stats.scripts++
continue
}
if (Resources.images.includes(ext)) {
if (find(Resources.images, ext) != null) {
stats.images++
continue
}
if (Resources.sounds.includes(ext)) {
if (find(Resources.sounds, ext) != null) {
stats.sounds++
continue
}
if (Resources.fonts.includes(ext)) {
if (find(Resources.fonts, ext) != null) {
stats.fonts++
continue
}

View File

@@ -700,7 +700,7 @@ function _load_image_file(path) {
var decoded
if (!bytes) return null
var ext = path.split('.').pop().toLowerCase()
var ext = lower(array(path, '.').pop())
var surface = null
switch (ext) {
@@ -758,6 +758,13 @@ sdl_gpu.get_texture = function(path) {
return tex
}
// Get texture info (dimensions) for a path
sdl_gpu.get_texture_info = function(path) {
var tex = sdl_gpu.get_texture(path)
if (!tex) return null
return {width: tex.width, height: tex.height}
}
// ========================================================================
// RENDER TARGET MANAGEMENT
// ========================================================================
@@ -844,7 +851,8 @@ function _build_sprite_vertices(sprites, camera) {
var white = {r: 1, g: 1, b: 1, a: 1}
array.for(sprites, s => {
for(var i = 0; i < sprites.length; i++) {
var s = sprites[i]
var px = s.pos.x
var py = s.pos.y
var w = s.width || 1
@@ -948,7 +956,7 @@ function _build_sprite_vertices(sprites, camera) {
index_data.w16(vertex_count + 3)
vertex_count += 4
})
}
return {
vertices: stone(vertex_data),

View File

@@ -9,30 +9,6 @@ var shape_proto = {
return this
},
set_groups: function(groups) {
var old_groups = this.groups
this.groups = groups
film2d.reindex(this._id, old_groups, groups)
return this
},
add_group: function(group) {
if (this.groups.indexOf(group) < 0) {
this.groups.push(group)
film2d.index_group(this._id, group)
}
return this
},
remove_group: function(group) {
var idx = this.groups.indexOf(group)
if (idx >= 0) {
this.groups.splice(idx, 1)
film2d.unindex_group(this._id, group)
}
return this
},
destroy: function() {
film2d.unregister(this._id)
}

View File

@@ -3,36 +3,6 @@ var film2d = use('film2d')
var sprite_proto = {
type: 'sprite',
set_pos: function(x, y) {
this.pos.x = x
this.pos.y = y
return this
},
set_groups: function(groups) {
var old_groups = this.groups
this.groups = groups
film2d.reindex(this._id, old_groups, groups)
return this
},
add_group: function(group) {
if (this.groups.indexOf(group) < 0) {
this.groups.push(group)
film2d.index_group(this._id, group)
}
return this
},
remove_group: function(group) {
var idx = this.groups.indexOf(group)
if (idx >= 0) {
this.groups.splice(idx, 1)
film2d.unindex_group(this._id, group)
}
return this
},
destroy: function() {
film2d.unregister(this._id)
}
@@ -44,7 +14,10 @@ return function(props) {
pos: {x: 0, y: 0},
image: null,
width: 1,
height: 1,
height: null,
fit: 'contain',
fit_anchor_x: 0.5,
fit_anchor_y: 0.5,
anchor_x: 0,
anchor_y: 0,
flip: {x: false, y: false},

View File

@@ -15,7 +15,7 @@ var Anim = (() => {
a.timer += dt;
def frames = a.src.frames;
while(true){
def time = number.max(frames[a.idx].time || 0, Anim.minDelay);
def time = max(frames[a.idx].time || 0, Anim.minDelay);
if(a.timer < time) break; /* still on current frame */
a.timer -= time;
@@ -98,7 +98,7 @@ function loop(){
}
/* schedule next tick: aim for 60 Hz but wont matter to anim speed */
$delay(loop, number.max(0, (1/60) - (os.now()-now)));
$delay(loop, max(0, (1/60) - (os.now()-now)));
}
loop();

View File

@@ -36,8 +36,8 @@ var center = [0.5,0.5]
var vel = 50
function hsl_to_rgb(h, s, l) {
var c = (1 - number.abs(2 * l - 1)) * s
var x = c * (1 - number.abs((h / 60) % 2 - 1))
var c = (1 - abs(2 * l - 1)) * s
var x = c * (1 - abs((h / 60) % 2 - 1))
var m = l - c / 2
var r = 0, g = 0, b = 0
@@ -75,15 +75,15 @@ function loop()
render.clear([22/255,120/255,194/255,255/255])
render.camera(camera)
sprite.forEach(x => x.move(x.dir.scale(dt)))
arrfor(sprite, x => x.move(x.dir.scale(dt)))
var queue = sprite.queue()
//log.console(queue)
for (var q of queue) {
if (!q.image) continue
arrfor(queue, q => {
if (!q.image) return
render.geometry(q.image.texture, q.mesh)
}
})
render.present()
dt = os.now() - now
@@ -93,7 +93,7 @@ function loop()
if (now - last_fps_update >= fps_update_period) {
var sum = 0
for (var i = 0; i < fps_samples.length; i++) sum += fps_samples[i]
arrfor(fps_samples, x => sum += x)
prosperon.window.title = `Bunnymark [fps: ${(fps_samples.length/sum).toFixed(1)}]`;
last_fps_update = now
}

View File

@@ -65,7 +65,7 @@ for (var cs in colorspaces) {
}
// Just test first 3 colorspaces
if (array(colorspaces).indexOf(cs) >= 2) break;
if (find(array(colorspaces), cs) >= 2) break;
}
log.console("\nColorspace test complete!");

View File

@@ -11,13 +11,13 @@ function make_engine(default_clock) {
this.tweens.push(tween)
},
remove(tween) {
this.tweens = this.tweens.filter(t => t != tween)
this.tweens = filter(this.tweens, t => t != tween)
},
update(current_time) {
if (current_time == null) {
current_time = this.default_clock ? this.default_clock() : time.number()
}
for (var tween of this.tweens.slice()) {
for (var tween of array(this.tweens)) {
tween._update(current_time)
}
},
@@ -92,7 +92,7 @@ var TweenProto = {
seek: function(global_time) {
var elapsed = global_time - this.startTime
var t = number.min(number.max(elapsed / this.duration, 0), 1)
var t = min(max(elapsed / this.duration, 0), 1)
var eased = this.easing(t)
for (var key in this.endVals) {
@@ -100,8 +100,8 @@ var TweenProto = {
var end = this.endVals[key]
var value = start + (end - start) * eased
if (key.includes('.')) {
var parts = key.split('.')
if (search(key, '.') != null) {
var parts = array(key, '.')
var objKey = parts[0]
var subKey = parts[1]
@@ -211,8 +211,8 @@ var TimelineProto = {
toJSON: function() {
return {
current_time: this.current_time,
events: this.events.map(e => ({ time: e.time, fired: e.fired })),
tweens: this.engine.tweens.map(t => t.toJSON())
events: array(this.events, e => ({ time: e.time, fired: e.fired })),
tweens: array(this.engine.tweens, t => t.toJSON())
}
}
}

View File

@@ -1,10 +1,19 @@
var world = {}
world.add_entity = function(entity_proto) {
var entity = meme(entity_proto)
var entities = {}
world.add_entity = function(entity_proto, data) {
var entity = meme(entity_proto, data)
if (is_function(entity.init))
entity.init()
entities[entity] = true
return entity
}
world.destroy_entity = function(entity) {
if (is_function(entity.on_destroy))
entity.on_destroy()
delete entities[entity]
}
return world