Some checks failed
Build and Deploy / build-macos (push) Failing after 5s
Build and Deploy / build-windows (CLANG64) (push) Has been cancelled
Build and Deploy / package-dist (push) Has been cancelled
Build and Deploy / deploy-itch (push) Has been cancelled
Build and Deploy / deploy-gitea (push) Has been cancelled
Build and Deploy / build-linux (push) Has been cancelled
477 lines
14 KiB
JavaScript
477 lines
14 KiB
JavaScript
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,
|
||
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)
|
||
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
|