var render = use('render') var graphics = use('graphics') var math = use('math') var util = use('util') var os = use('os') var geometry = use('geometry') 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. ` /*var whiteimage = {} whiteimage = graphics.make_surface([1,1]) whiteimage.rect({x:0,y:0,width:1,height:1}, [1,1,1,1]) render.load_texture(whiteimage) */ if (render.point) draw.point = function(pos,size,opt = {color:Color.white}, pipeline) { render.settings(opt) render.pipeline(pipeline) render.point([pos]) } else draw.point = function() { throw new Error('Backend cannot draw points.') } 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) render.point(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) render.rects(strips) } var ellipse_def = { color: Color.white, start: 0, end: 1, mode: 'fill', thickness: 1, } if (render.ellipse) draw.ellipse = function(pos, radii, def, pipeline) { } else draw.ellipse = function(pos, radii, def, pipeline) { var opt = def ? {...ellipse_def, ...def} : ellipse_def render.settings(opt) render.pipeline(pipeline) 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 render.settings(opt) render.pipeline(pipeline) render.line(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) { render.rectangle(rect); return; } /* stroke swallows the whole thing → fill instead */ if ((thickness << 1) >= rect.width || (thickness << 1) >= rect.height) { render.rectangle(rect) // filled return } const x0 = rect.x, y0 = rect.y, x1 = rect.x + rect.width, y1 = rect.y + rect.height render.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) ------------------------- */ render.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 ) } render.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 */ render.rects([ { x:x0 + radius, y:y0, width:rect.width - (radius << 1), height:rect.height } ]) /* side columns */ render.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 } ) } render.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 render.settings(opt) render.pipeline(pipeline) var t = opt.thickness|0 if (t <= 0) { if (opt.radius) software_fill_round_rect(rect, opt.radius) else render.rectangle(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) render.slice9(image, rect, slice, slice9_info, pipeline); } 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, } 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) rect.width ??= image.texture.width rect.height ??= image.texture.height info ??= image_info; render.settings(info) render.image(image, rect, rotation, anchor, shear, info) } 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) render.geometry(font, mesh) } 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