var prosperon = {} var os = use('os'); var io = use('io'); var rasterize = use('rasterize'); var time = use('time') var tilemap = use('tilemap') var geometry = use('geometry') var res = use('resources') var video = arg[0] var graphics = use('graphics', arg[0]) var camera = {} function updateCameraMatrix(cam) { def win_w = logical.width def win_h = logical.height def view_w = (cam.size?.[0] ?? win_w) / cam.zoom def view_h = (cam.size?.[1] ?? win_h) / cam.zoom def ox = cam.pos[0] - view_w * (cam.anchor?.[0] ?? 0) def oy = cam.pos[1] - view_h * (cam.anchor?.[1] ?? 0) def vx = (cam.viewport?.x ?? 0) * win_w def vy = (cam.viewport?.y ?? 0) * win_h def vw = (cam.viewport?.width ?? 1) * win_w def vh = (cam.viewport?.height ?? 1) * win_h def sx = vw / view_w def sy = vh / view_h // flip-Y later /* affine matrix that SDL wants (Y going down) */ cam.a = sx cam.c = vx - sx * ox cam.e = -sy // <-- minus = flip Y cam.f = vy + vh + sy * oy /* convenience inverses */ cam.ia = 1 / cam.a cam.ic = -cam.c / cam.a cam.ie = 1 / cam.e cam.if = -cam.f / cam.e camera = cam } //---- forward transform ---- function worldToScreenPoint([x,y], camera) { return { x: camera.a * x + camera.c, y: camera.e * y + camera.f }; } //---- inverse transform ---- function screenToWorldPoint(pos, camera) { return { x: camera.ia * pos[0] + camera.ic, y: camera.ie * pos[1] + camera.if }; } //---- rectangle (two corner) ---- function worldToScreenRect({x,y,width,height}, camera) { // map bottom-left and top-right def x1 = camera.a * x + camera.c; def y1 = camera.e * y + camera.f; def x2 = camera.a * (x + width) + camera.c; def y2 = camera.e * (y + height) + camera.f; return { x:Math.min(x1,x2), y:Math.min(y1,y2), width:Math.abs(x2-x1), height:Math.abs(y2-y1) } } var gameactor var images = {} var renderer_commands = [] var win_size = {width:500,height:500} var logical = {width:500,height:500} // Convert high-level draw commands to low-level renderer commands function translate_draw_commands(commands) { if (!graphics) return renderer_commands.length = 0 commands.forEach(function(cmd) { if (cmd.material && cmd.material.color) { renderer_commands.push({ op: "set", prop: "drawColor", value: cmd.material.color }) } switch(cmd.cmd) { case "camera": updateCameraMatrix(cmd.camera, win_size.width, win_size.height) break case "draw_rect": cmd.rect = worldToScreenRect(cmd.rect, camera) // Handle rectangles with optional rounding and thickness if (cmd.opt && cmd.opt.radius && cmd.opt.radius > 0) { // Rounded rectangle var thickness = (cmd.opt.thickness == 0) ? 0 : (cmd.opt.thickness || 1) var raster_result = rasterize.round_rect(cmd.rect, cmd.opt.radius, thickness) if (raster_result.type == 'rect') { renderer_commands.push({ op: "fillRect", data: {rect: raster_result.data} }) } else if (raster_result.type == 'rects') { raster_result.data.forEach(function(rect) { renderer_commands.push({ op: "fillRect", data: {rect: rect} }) }) } } else if (cmd.opt && cmd.opt.thickness && cmd.opt.thickness > 0) { // Outlined rectangle var raster_result = rasterize.outline_rect(cmd.rect, cmd.opt.thickness) if (raster_result.type == 'rect') { renderer_commands.push({ op: "fillRect", data: {rect: raster_result.data} }) } else if (raster_result.type == 'rects') { renderer_commands.push({ op: "rects", data: {rects: raster_result.data} }) } } else { renderer_commands.push({ op: "fillRect", data: {rect: cmd.rect} }) } break case "draw_circle": case "draw_ellipse": cmd.pos = worldToScreenPoint(cmd.pos, camera) // Rasterize ellipse to points or rects var radii = cmd.radii || [cmd.radius, cmd.radius] var raster_result = rasterize.ellipse(cmd.pos, radii, cmd.opt || {}) if (raster_result.type == 'points') { renderer_commands.push({ op: "point", data: {points: raster_result.data} }) } else if (raster_result.type == 'rects') { // Use 'rects' operation for multiple rectangles renderer_commands.push({ op: "rects", data: {rects: raster_result.data} }) } break case "draw_line": renderer_commands.push({ op: "line", data: {points: cmd.points.map(p => { var pt = worldToScreenPoint(p, camera) return [pt.x, pt.y] })} }) break case "draw_point": cmd.pos = worldToScreenPoint(cmd.pos, camera) renderer_commands.push({ op: "point", data: {points: [cmd.pos]} }) break case "draw_image": var img = graphics.texture(cmd.image) var gpu = img.gpu if (!gpu) break if (!cmd.scale) cmd.scale = {x:1,y:1} cmd.rect.width ??= img.width cmd.rect.height ??= img.height cmd.rect.width = cmd.rect.width * cmd.scale.x cmd.rect.height = cmd.rect.height * cmd.scale.y cmd.rect = worldToScreenRect(cmd.rect, camera) renderer_commands.push({ op: "texture", data: { texture_id: gpu, dst: cmd.rect, src: img.rect } }) break case "draw_text": if (!cmd.text) break if (!cmd.pos) break // Get font from the font string (e.g., "smalle.16") var font = graphics.get_font(cmd.font) if (!font || !font.texture || !font.texture.id) break // Create text geometry buffer var text_mesh = graphics.make_text_buffer( cmd.text, {x: cmd.pos.x, y: cmd.pos.y}, [cmd.material.color.r, cmd.material.color.g, cmd.material.color.b, cmd.material.color.a], cmd.wrap || 0, font ) if (!text_mesh) break if (text_mesh.xy.length == 0) break // Transform XY coordinates using camera matrix var camera_params = [camera.a, camera.c, camera.e, camera.f] var transformed_xy = geometry.transform_xy_blob(text_mesh.xy, camera_params) // Create transformed geometry object var transformed_geom = { xy: transformed_xy, xy_stride: text_mesh.xy_stride, uv: text_mesh.uv, uv_stride: text_mesh.uv_stride, color: text_mesh.color, color_stride: text_mesh.color_stride, indices: text_mesh.indices, num_vertices: text_mesh.num_vertices, num_indices: text_mesh.num_indices, size_indices: text_mesh.size_indices, texture_id: font.texture.id } renderer_commands.push({ op: "geometry_raw", data: transformed_geom }) break case "draw_slice9": var img = graphics.texture(cmd.image) var gpu = img.gpu if (!gpu) break cmd.rect = worldToScreenRect(cmd.rect, camera) renderer_commands.push({ op: "texture9Grid", data: { texture_id: gpu, src: img.rect, leftWidth: cmd.slice, rightWidth: cmd.slice, topHeight: cmd.slice, bottomHeight: cmd.slice, scale: 1.0, dst: cmd.rect } }) break case "tilemap": // Get cached geometry commands from tilemap var geometryCommands = cmd.tilemap.draw() // Process each geometry command (one per texture) for (var geomCmd of geometryCommands) { var img = graphics.texture(geomCmd.image) var gpu = img.gpu if (!gpu) continue // Transform geometry through camera and send to renderer var geom = geomCmd.geometry // Transform XY coordinates using camera matrix var camera_params = [camera.a, camera.c, camera.e, camera.f] var transformed_xy = geometry.transform_xy_blob(geom.xy, camera_params) // Create new geometry object with transformed coordinates var transformed_geom = { xy: transformed_xy, xy_stride: geom.xy_stride, uv: geom.uv, uv_stride: geom.uv_stride, color: geom.color, color_stride: geom.color_stride, indices: geom.indices, num_vertices: geom.num_vertices, num_indices: geom.num_indices, size_indices: geom.size_indices, texture_id: gpu } renderer_commands.push({ op: "geometry_raw", data: transformed_geom }) } break case "geometry": var texture_id if (cmd.texture_id) { // Use the provided texture ID directly texture_id = cmd.texture_id } else { // Fall back to looking up by image path var img = graphics.texture(cmd.image) var gpu = img.gpu if (!gpu) break texture_id = gpu } // Transform geometry through camera and send to renderer var geom = cmd.geometry // Transform XY coordinates using camera matrix var camera_params = [camera.a, camera.c, camera.e, camera.f] var transformed_xy = geometry.transform_xy_blob(geom.xy, camera_params) // Create new geometry object with transformed coordinates var transformed_geom = { xy: transformed_xy, xy_stride: geom.xy_stride, uv: geom.uv, uv_stride: geom.uv_stride, color: geom.color, color_stride: geom.color_stride, indices: geom.indices, num_vertices: geom.num_vertices, num_indices: geom.num_indices, size_indices: geom.size_indices, texture_id: texture_id } renderer_commands.push({ op: "geometry_raw", data: transformed_geom }) break } }) return renderer_commands } ///// input ///// var input = use('input') var input_cb var input_rate = 1/60 function poll_input() { send(video, {kind:'input', op:'get'}, evs => { for (var ev of evs) { if (ev.type == 'window_pixel_size_changed') { win_size.width = ev.width win_size.height = ev.height } if (ev.type == 'quit') $_.stop() if (ev.type.includes('key')) { if (ev.key) ev.key = input.keyname(ev.key) } if (ev.type.startsWith('mouse_')) ev.pos.y = -ev.pos.y + logical.height } input_cb(evs) }) $_.delay(poll_input, input_rate) } prosperon.input = function(fn) { input_cb = fn poll_input() } // 2) helper to build & send a batch, then call done() prosperon.create_batch = function create_batch(draw_cmds, done) { def batch = [ {op:'set', prop:'drawColor', value:{r:0.1,g:0.1,b:0.15,a:1}}, {op:'clear'} ] if (draw_cmds && draw_cmds.length) batch.push(...translate_draw_commands(draw_cmds)) batch.push( {op:'set', prop:'drawColor', value:{r:1,g:1,b:1,a:1}}, {op:'imgui_render'}, {op:'present'} ) send(video, {kind:'renderer', op:'batch', data:batch}, done) } ////////// dmon hot reload //////// function poll_file_changes() { dmon.poll(e => { if (e.action == 'modify' || e.action == 'create') { // Check if it's an image file var ext = e.file.split('.').pop().toLowerCase() var imageExts = ['png', 'jpg', 'jpeg', 'gif', 'bmp', 'tga', 'webp', 'qoi', 'ase', 'aseprite'] if (imageExts.includes(ext)) { // Try to find the full path for this image var possiblePaths = [ e.file, e.root + e.file, res.find_image(e.file.split('/').pop().split('.')[0]) ].filter(p => p) for (var path of possiblePaths) { graphics.tex_hotreload(path) } } } }) // Schedule next poll in 0.5 seconds $_.delay(poll_file_changes, 0.5) } var dmon = use('dmon') prosperon.dmon = function() { dmon.watch('.') poll_file_changes() } var window_cmds = { size(size) { send(video, {kind: 'window', op:'set', data: {property: 'size', value: size}}) }, } prosperon.set_window = function(config) { for (var c in config) if (window_cmds[c]) window_cmds[c](config[c]) } var renderer_cmds = { resolution(e) { logical.width = e.width logical.height = e.height send(video, {kind:"renderer", op:'set', prop:'logicalPresentation', value: {...e}}) } } prosperon.set_renderer = function(config) { for (var c in config) if (renderer_cmds[c]) renderer_cmds[c](config[c]) } return prosperon