var renderer_actor = arg[0] var renderer_id = arg[1] var graphics = use('graphics', renderer_actor, renderer_id) var math = use('math') var util = use('util') var os = use('os') var geometry = use('geometry') var Color = use('color') var draw = {} draw[prosperon.DOC] = ` A collection of 2D drawing functions that operate in screen space. Provides primitives for lines, rectangles, text, sprite drawing, etc. Immediate mode. ` // Draw command accumulator var commands = [] // Prototype object for commands var command_proto = { kind: "renderer", id: renderer_id } // Clear accumulated commands draw.clear = function() { commands = [] } // Get accumulated commands draw.get_commands = function() { return commands } // Flush all commands to renderer draw.flush = function() { if (!renderer_actor || !renderer_id || commands.length === 0) return // Convert commands to batch format var batch_data = commands.map(function(cmd) { return { op: cmd.op, prop: cmd.prop, value: cmd.value, data: cmd.data } }) // Send all commands in a single batch message send(renderer_actor, { kind: "renderer", id: renderer_id, op: "batch", data: batch_data }) // Clear commands after sending commands = [] } // Helper to add a command function add_command(op, data, prop, value) { var cmd = Object.create(command_proto) cmd.op = op if (data) cmd.data = data if (prop) cmd.prop = prop if (value !== undefined) cmd.value = value commands.push(cmd) } // Drawing functions draw.point = function(pos, size, opt = {color: Color.white}, pipeline) { if (opt.color) { add_command("set", null, "drawColor", opt.color) } add_command("point", {points: [pos]}) } draw.point[prosperon.DOC] = ` :param pos: A 2D position ([x, y]) where the point should be drawn. :param size: The size of the point. :param color: The color of the point, defaults to white. :return: None ` /* ------------------------------------------------------------------------ * helper – is (dx,dy) inside the desired wedge? * -------------------------------------------------------------------- */ function within_wedge(dx, dy, start, end, full_circle) { if (full_circle) return true var ang = Math.atan2(dy, dx) // [-π,π] if (ang < 0) ang += Math.PI * 2 var t = ang / (Math.PI * 2) // turn ∈ [0,1) if (start <= end) return t >= start && t <= end return t >= start || t <= end // wrap-around arc } /* ------------------------------------------------------------------------ * software ellipse – outline / ring / fill via inner_radius * -------------------------------------------------------------------- */ function software_ellipse(pos, radii, opt) { var rx = radii[0], ry = radii[1] if (rx <= 0 || ry <= 0) return var cx = pos[0], cy = pos[1] var raw_start = opt.start var raw_end = opt.end var full_circle = Math.abs(raw_end - raw_start) >= 1 - 1e-9 var start = (raw_start % 1 + 1) % 1 var end = (raw_end % 1 + 1) % 1 var thickness = Math.max(1, opt.thickness|0) /* inner ellipse radii (may be zero or negative → treat as no hole) */ var rx_i = rx - thickness, ry_i = ry - thickness var hole = (rx_i > 0 && ry_i > 0) /* fast one-pixel outline ? --------------------------------------- */ if (!hole && thickness === 1) { /* (same midpoint algorithm as before, filtered by wedge) --------*/ var rx_sq = rx * rx, ry_sq = ry * ry var two_rx_sq = rx_sq << 1, two_ry_sq = ry_sq << 1 var x = 0, y = ry, px = 0, py = two_rx_sq * y var p = ry_sq - rx_sq * ry + 0.25 * rx_sq function plot_pts(x, y) { 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)) if (pts.length) { add_command("point", {points: pts}) } } while (px < py) { plot_pts(x, y) ++x; px += two_ry_sq if (p < 0) p += ry_sq + px else { --y; py -= two_rx_sq; p += ry_sq + px - py } } p = ry_sq*(x+.5)*(x+.5) + rx_sq*(y-1)*(y-1) - rx_sq*ry_sq while (y >= 0) { plot_pts(x, y) --y; py -= two_rx_sq if (p > 0) p += rx_sq - py else { ++x; px += two_ry_sq; p += rx_sq - py + px } } return } /* ------------------------------------------------------------------ * FILL or RING (thickness ≥ 2 OR hole == false -> full fill) * ---------------------------------------------------------------- */ 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 for (var dy = -ry; dy <= ry; ++dy) { var yy = dy * dy var x_out = Math.floor(rx * Math.sqrt(1 - yy / ry_sq)) var y_screen = cy + dy /* optional inner span */ var x_in = hole ? Math.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 && Math.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 var last = (dx === x_out) var next_in_ring = !last && !(hole && Math.abs(dx+1) <= x_in) && within_wedge(dx+1, dy, start, end, full_circle) if (last || !next_in_ring) { strips.push({ x: run_start, y: y_screen, width: (cx + dx) - run_start + 1, height: 1 }) run_start = null } } } if (strips.length) { add_command("rects", {rects: strips}) } } var ellipse_def = { color: Color.white, start: 0, end: 1, mode: 'fill', thickness: 1, } draw.ellipse = function(pos, radii, def, pipeline) { var opt = def ? {...ellipse_def, ...def} : ellipse_def if (opt.color) { add_command("set", null, "drawColor", opt.color) } if (opt.thickness <= 0) opt.thickness = Math.max(radii[0], radii[1]) software_ellipse(pos, radii, opt) } var line_def = { color: Color.white, thickness: 1, cap:"butt", } draw.line = function(points, def, pipeline) { var opt if (def) opt = {...line_def, ...def} else opt = line_def if (opt.color) { add_command("set", null, "drawColor", opt.color) } add_command("line", {points: points}) } draw.cross = function render_cross(pos, size, def, pipe) { var a = [pos.add([0, size]), pos.add([0, -size])] var b = [pos.add([size, 0]), pos.add([-size, 0])] draw.line(a, def, pipe) draw.line(b, def,pipe) } draw.arrow = function render_arrow(start, end, wingspan = 4, wingangle = 10, def, pipe) { var dir = math.norm(end.sub(start)) var wing1 = [math.rotate(dir, wingangle).scale(wingspan).add(end), end] var wing2 = [math.rotate(dir, -wingangle).scale(wingspan).add(end), end] draw.line([start, end], def, pipe) draw.line(wing1, def, pipe) draw.line(wing2, def, pipe) } /* ------------------------------------------------------------------------ * helper – plain rectangle outline of arbitrary thickness (radius=0) * -------------------------------------------------------------------- */ function software_outline_rect(rect, thickness) { if (thickness <= 0) { add_command("fillRect", {rect: rect}) return; } /* stroke swallows the whole thing → fill instead */ if ((thickness << 1) >= rect.width || (thickness << 1) >= rect.height) { add_command("fillRect", {rect: rect}) return } const x0 = rect.x, y0 = rect.y, x1 = rect.x + rect.width, y1 = rect.y + rect.height add_command("rects", { rects: [ { x:x0, y:y0, width:rect.width, height:thickness }, // top { x:x0, y:y1-thickness, width:rect.width, height:thickness }, // bottom { x:x0, y:y0+thickness, width:thickness, height:rect.height - (thickness<<1) }, // left { x:x1-thickness, y:y0+thickness, width:thickness, height:rect.height - (thickness<<1) } // right ] }) } /* ------------------------------------------------------------------------ * ROUNDED rectangle outline (was software_round_rect) * -------------------------------------------------------------------- */ function software_round_rect(rect, radius, thickness = 1) { if (thickness <= 0) { software_fill_round_rect(rect, radius) return } radius = Math.min(radius, rect.width >> 1, rect.height >> 1) /* stroke covers whole rect → fall back to fill ------------------- */ if ((thickness << 1) >= rect.width || (thickness << 1) >= rect.height || thickness >= radius) { software_fill_round_rect(rect, radius) return } const x0 = rect.x, y0 = rect.y, x1 = rect.x + rect.width - 1, // inclusive y1 = rect.y + rect.height - 1 const cx_l = x0 + radius, cx_r = x1 - radius const cy_t = y0 + radius, cy_b = y1 - radius const r_out = radius const r_in = radius - thickness /* straight bands (top/bottom/left/right) ------------------------- */ add_command("rects", { rects: [ { x:x0 + radius, y:y0, width:rect.width - (radius << 1), height:thickness }, // top { x:x0 + radius, y:y1 - thickness + 1, width:rect.width - (radius << 1), height:thickness }, // bottom { x:x0, y:y0 + radius, width:thickness, height:rect.height - (radius << 1) }, // left { x:x1 - thickness + 1, y:y0 + radius, width:thickness, height:rect.height - (radius << 1) } // right ] }) /* corner arcs ---------------------------------------------------- */ const strips = [] for (let dy = 0; dy < radius; ++dy) { const dy_sq = dy * dy const dx_out = Math.floor(Math.sqrt(r_out * r_out - dy_sq)) const dx_in = (r_in > 0 && dy < r_in) ? Math.floor(Math.sqrt(r_in * r_in - dy_sq)) : -1 // no inner rim const w = dx_out - dx_in // strip width if (w <= 0) continue /* top */ strips.push( { x:cx_l - dx_out, y:cy_t - dy, width:w, height:1 }, // NW { x:cx_r + dx_in + 1, y:cy_t - dy, width:w, height:1 } // NE ) /* bottom */ strips.push( { x:cx_l - dx_out, y:cy_b + dy, width:w, height:1 }, // SW { x:cx_r + dx_in + 1, y:cy_b + dy, width:w, height:1 } // SE ) } add_command("rects", {rects: strips}) } /* ------------------------------------------------------------------------ * filled rounded rect (unchanged) * -------------------------------------------------------------------- */ function software_fill_round_rect(rect, radius) { radius = Math.min(radius, rect.width >> 1, rect.height >> 1) const x0 = rect.x, y0 = rect.y, x1 = rect.x + rect.width - 1, y1 = rect.y + rect.height - 1 /* main column */ add_command("rects", { rects: [ { x:x0 + radius, y:y0, width:rect.width - (radius << 1), height:rect.height } ] }) /* side columns */ add_command("rects", { rects: [ { 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) } ] }) /* corner caps */ const cx_l = x0 + radius, cx_r = x1 - radius const cy_t = y0 + radius, cy_b = y1 - radius const caps = [] for (let dy = 0; dy < radius; ++dy) { const dx = Math.floor(Math.sqrt(radius * radius - dy * dy)) const w = (dx << 1) + 1 caps.push( { x:cx_l - dx, y:cy_t - dy, width:w, height:1 }, { x:cx_r - dx, y:cy_t - dy, width:w, height:1 }, { x:cx_l - dx, y:cy_b + dy, width:w, height:1 }, { x:cx_r - dx, y:cy_b + dy, width:w, height:1 } ) } add_command("rects", {rects: caps}) } var rect_def = { thickness:1, color: Color.white, radius: 0 } draw.rectangle = function render_rectangle(rect, def, pipeline) { var opt = def ? {...rect_def, ...def} : rect_def if (opt.color) { add_command("set", null, "drawColor", opt.color) } var t = opt.thickness|0 if (t <= 0) { if (opt.radius) software_fill_round_rect(rect, opt.radius) else add_command("fillRect", {rect: rect}) return } if (opt.radius) software_round_rect(rect, opt.radius, t) else software_outline_rect(rect,t) } var slice9_info = { tile_top:true, tile_bottom:true, tile_left:true, tile_right:true, tile_center_x:true, tile_center_right:true, color: Color.white, } draw.slice9 = function slice9(image, rect = [0,0], slice = 0, info = slice9_info, pipeline) { if (!image) throw Error('Need an image to render.') if (typeof image === "string") image = graphics.texture(image) // TODO: Implement slice9 rendering via SDL video actor } draw.slice9[prosperon.DOC] = ` :param image: An image object or string path to a texture. :param rect: A rectangle specifying draw location/size, default [0, 0]. :param slice: The pixel inset or spacing for the 9-slice (number or object). :param info: A slice9 info object controlling tiling of edges/corners. :param pipeline: (Optional) A pipeline or rendering state object. :return: None :raises Error: If no image is provided. ` var std_sprite_cmd = {type:'sprite', color:[1,1,1,1]} var image_info = { tile_x: false, tile_y: false, flip_x: false, flip_y: false, color: Color.white, mode: 'linear' } draw.image = function image(image, rect = [0,0], rotation = 0, anchor = [0,0], shear = [0,0], info = {}, pipeline) { if (!image) throw Error('Need an image to render.') if (typeof image === "string") image = graphics.texture(image) // Ensure rect has proper structure if (Array.isArray(rect)) { rect = {x: rect[0], y: rect[1], width: image.width, height: image.height} } else { rect.width ??= image.width rect.height ??= image.height } info = Object.assign({}, image_info, info); // Get the GPU texture (might be loading) var texture = image.gpu; if (!texture) { // Texture not loaded yet, skip drawing return; } // Set texture filtering mode if (info.mode) { add_command("set", null, "textureFilter", info.mode === 'linear' ? 'linear' : 'nearest') } // Set color if specified if (info.color) { add_command("set", null, "drawColor", info.color) } // Calculate source rectangle from image.rect (UV coords) var src_rect = { x: image.rect.x * image.width, y: image.rect.y * image.height, width: image.rect.width * image.width, height: image.rect.height * image.height } // Handle flipping if (info.flip_x) { src_rect.x += src_rect.width; src_rect.width = -src_rect.width; } if (info.flip_y) { src_rect.y += src_rect.height; src_rect.height = -src_rect.height; } // Draw the texture add_command("copyTexture", { texture_id: texture.id, src: src_rect, dest: rect }) } function software_circle(pos, radius) { if (radius <= 0) return // nothing to draw var cx = pos[0], cy = pos[1] var x = 0, y = radius var d = 3 - (radius << 1) // decision parameter while (x <= y) { draw.point([ [cx + x, cy + y], [cx - x, cy + y], [cx + x, cy - y], [cx - x, cy - y], [cx + y, cy + x], [cx - y, cy + x], [cx + y, cy - x], [cx - y, cy - x] ]) if (d < 0) d += (x << 2) + 6 else { d += ((x - y) << 2) + 10 y-- } x++ } } var circle_def = { inner_radius:1, // percentage: 1 means filled circle color: Color.white, start:0, end: 1, } draw.circle = function render_circle(pos, radius, def, pipeline) { draw.ellipse(pos, [radius,radius], def, pipeline) } var sysfont = graphics.get_font('fonts/c64.ttf', 8) draw.text = function text(text, rect, font = sysfont, size = 0, color = Color.white, wrap = 0, pipeline) { if (typeof font === 'string') font = graphics.get_font(font) var mesh = graphics.make_text_buffer(text, rect, 0, color, wrap, font) // TODO: Handle text rendering via geometry } draw.text[prosperon.DOC] = ` :param text: The string to draw. :param rect: A rectangle specifying draw position (and possibly wrapping area). :param font: A font object or string path, default sysfont. :param size: (Unused) Possibly intended for scaling the font size. :param color: The text color, default Color.white. :param wrap: Pixel width for text wrapping, default 0 (no wrap). :param pipeline: (Optional) A pipeline or rendering state object. :return: None ` return draw