Files
cell/scripts/modules/draw2d.js

586 lines
16 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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