diff --git a/.cell/cell.toml b/.cell/cell.toml index fdf5e83b..5fe113a2 100644 --- a/.cell/cell.toml +++ b/.cell/cell.toml @@ -15,3 +15,5 @@ main = true main = true [actors.prosperon] main = true +[actors.accio] +main=true \ No newline at end of file diff --git a/prosperon/prosperon.cm b/prosperon/prosperon.cm new file mode 100644 index 00000000..ffb67f0f --- /dev/null +++ b/prosperon/prosperon.cm @@ -0,0 +1,426 @@ +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 + + cmd.rect.width ??= img.width + cmd.rect.height ??= img.height + cmd.rect = worldToScreenRect(cmd.rect, camera) + + renderer_commands.push({ + op: "texture", + data: { + texture_id: gpu.id, + dst: cmd.rect, + src: img.rect + } + }) + + break + + case "draw_text": + if (!cmd.text) break + if (!cmd.pos) break + var rect = worldToScreenRect({x:cmd.pos.x, y:cmd.pos.y, width:8, height:8}, camera) + var pos = {x: rect.x, y: rect.y} + renderer_commands.push({ + op: "debugText", + data: { + pos, + text: cmd.text + } + }) + 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.id, + src: img.rect, + leftWidth: cmd.slice, + rightWidth: cmd.slice, + topHeight: cmd.slice, + bottomHeight: cmd.slice, + scale: 1.0, + dst: cmd.rect + } + }) + break + + case "tilemap": + // Group tiles by texture to batch draw calls + var textureGroups = {} + var tilePositions = [] + + // Collect all tiles and their positions + tilemap.for(cmd.tilemap, (tile, {x,y}) => { + if (tile) { + tilePositions.push({tile, x, y}) + } + }) + + // Group tiles by texture + tilePositions.forEach(({tile, x, y}) => { + var img = graphics.texture(tile) + if (img && img.gpu) { + var texId = img.gpu.id + if (!textureGroups[texId]) { + textureGroups[texId] = { + texture: img, + tiles: [] + } + } + textureGroups[texId].tiles.push({x, y, img}) + } + }) + + // Generate draw commands for each texture group + Object.keys(textureGroups).forEach(texId => { + var group = textureGroups[texId] + var tiles = group.tiles + + // Create a temporary tilemap with only tiles from this texture + // Apply tilemap position to the offset to shift the world coordinates + var tempMap = { + tiles: [], + offset_x: cmd.tilemap.offset_x + (cmd.tilemap.pos.x / cmd.tilemap.size_x), + offset_y: cmd.tilemap.offset_y + (cmd.tilemap.pos.y / cmd.tilemap.size_y), + size_x: cmd.tilemap.size_x, + size_y: cmd.tilemap.size_y + } + + // Build sparse array for this texture's tiles + tiles.forEach(({x, y, img}) => { + var arrayX = x - cmd.tilemap.offset_x + var arrayY = y - cmd.tilemap.offset_y + if (!tempMap.tiles[arrayX]) tempMap.tiles[arrayX] = [] + tempMap.tiles[arrayX][arrayY] = img + }) + // Generate geometry for this texture group + var geom = geometry.tilemap_to_data(cmd.tilemap) + geom.texture_id = parseInt(texId) + + renderer_commands.push({ + op: "geometry_raw", + data: 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(size) { + 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 diff --git a/prosperon/tilemap.cm b/prosperon/tilemap.cm index e3efc7f5..84b9f764 100644 --- a/prosperon/tilemap.cm +++ b/prosperon/tilemap.cm @@ -10,7 +10,7 @@ function tilemap() return this; } -tilemap.for = function (map, fn) { +tilemap.for = function tilemap_for(map, fn) { for (var x = 0; x < map.tiles.length; x++) { if (!map.tiles[x]) continue; for (var y = 0; y < map.tiles[x].length; y++) { diff --git a/prosperon/graphics.cm b/scripts/graphics.cm similarity index 75% rename from prosperon/graphics.cm rename to scripts/graphics.cm index d28c40e4..797e43c7 100644 --- a/prosperon/graphics.cm +++ b/scripts/graphics.cm @@ -20,6 +20,11 @@ var LOADING = Symbol() var cache = {} +graphics.setup = function(renderer) +{ + renderer_actor = renderer +} + // Image constructor function graphics.Image = function(surfaceData) { // Initialize private properties @@ -402,12 +407,6 @@ graphics.get_font = function get_font(path, size) { return font } -graphics.get_font[cell.DOC] = ` -:param path: A string path to a font file, optionally with ".size" appended. -:param size: Pixel size of the font, if not included in 'path'. -:return: A font object with .surface and .texture for rendering text. -Load a font from file if not cached, or retrieve from cache if already loaded. -` graphics.queue_sprite_mesh = function(queue) { var sprites = queue.filter(x => x.type == 'sprite') @@ -420,72 +419,5 @@ graphics.queue_sprite_mesh = function(queue) { } return [mesh.pos, mesh.uv, mesh.color, mesh.indices] } -graphics.queue_sprite_mesh[cell.DOC] = ` -:param queue: An array of draw commands, some of which are {type:'sprite'} objects. -:return: An array of references to GPU buffers [pos,uv,color,indices]. -Builds a single geometry mesh for all sprite-type commands in the queue, storing first_index/num_indices -so they can be rendered in one draw call. -` - -graphics.make_text_buffer[cell.DOC] = ` -:param text: The string to render. -:param rect: A rectangle specifying position and possibly wrapping. -:param angle: Rotation angle (unused or optional). -:param color: A color for the text (could be a vec4). -:param wrap: The width in pixels to wrap text, or 0 for no wrap. -:param font: A font object created by graphics.make_font or graphics.get_font. -:return: A geometry buffer mesh (pos, uv, color, indices) for rendering text. -Generate a GPU buffer mesh of text quads for rendering with a font, etc. -` - -graphics.rectpack[cell.DOC] = ` -:param width: The width of the area to pack into. -:param height: The height of the area to pack into. -:param sizes: An array of [w,h] pairs for the rectangles to pack. -:return: An array of [x,y] coordinates placing each rect, or null if they don't fit. -Perform a rectangle packing using the stbrp library. Return positions for each rect. -` - -graphics.make_texture[cell.DOC] = ` -:param data: Raw image bytes (PNG, JPG, etc.) as an ArrayBuffer. -:return: An SDL_Surface object representing the decoded image in RAM, for use with GPU or software rendering. -Convert raw image bytes into an SDL_Surface object. -` - -graphics.make_gif[cell.DOC] = ` -:param data: An ArrayBuffer containing GIF data. -:return: An object with frames[], each frame having its own .surface. Some also have a .texture for GPU use. -Load a GIF, returning its frames. If it's a single-frame GIF, the result may have .surface only. -` - -graphics.make_aseprite[cell.DOC] = ` -:param data: An ArrayBuffer containing Aseprite (ASE) file data. -:return: An object containing frames or animations, each with .surface. May also have top-level .surface for a single-layer case. -Load an Aseprite/ASE file from an array of bytes, returning frames or animations. -` - -graphics.cull_sprites[cell.DOC] = ` -:param sprites: An array of sprite objects (each has rect or transform). -:param camera: A camera or bounding rectangle defining the view area. -:return: A new array of sprites that are visible in the camera's view. -Filter an array of sprites to only those visible in the provided camera’s view. -` - -graphics.make_font[cell.DOC] = ` -:param data: TTF/OTF file data as an ArrayBuffer. -:param size: Pixel size for rendering glyphs. -:return: A font object with surface, texture, and glyph data, for text rendering with make_text_buffer. -Load a font from TTF/OTF data at the given size. -` - -graphics.make_line_prim[cell.DOC] = ` -:param points: An array of [x,y] points forming the line. -:param thickness: The thickness (width) of the polyline. -:param startCap: (Unused) Possibly the type of cap for the start. -:param endCap: (Unused) Possibly the type of cap for the end. -:param color: A color to apply to the line. -:return: A geometry mesh object suitable for rendering the line via a pipeline command. -Build a GPU mesh representing a thick polyline from an array of points, using parsl or a similar library under the hood. -` return graphics