draw2d now generates high level commands; turned into instructions by moth

This commit is contained in:
2025-05-27 13:46:56 -05:00
parent ad182d68ec
commit 01df337ccc
16 changed files with 797 additions and 630 deletions

View File

@@ -5,6 +5,7 @@ var sprite = use('sprite')
var geom = use('geometry')
var input = use('controller')
var config = use('config')
var color = use('color')
var bunnyTex = graphics.texture("bunny")
@@ -65,5 +66,5 @@ this.hud = function() {
draw.images(bunnyTex, bunnies)
var msg = 'FPS: ' + fpsAvg.toFixed(2) + ' Bunnies: ' + bunnies.length
draw.text(msg, {x:0, y:0, width:config.width, height:40}, undefined, 0, Color.white, 0)
draw.text(msg, {x:0, y:0, width:config.width, height:40}, undefined, 0, color.white, 0)
}

View File

@@ -2,6 +2,7 @@
var draw = use('draw2d')
var input = use('controller')
var config = use('config')
var color = use('color')
prosperon.camera.transform.pos = [0,0]
@@ -73,13 +74,13 @@ this.hud = function() {
draw.rectangle({x:0, y:0, width:config.width, height:config.height}, [0,0,0,1])
// Draw paddles
draw.rectangle({x:p1.x - paddleW*0.5, y:p1.y - paddleH*0.5, width:paddleW, height:paddleH}, Color.white)
draw.rectangle({x:p2.x - paddleW*0.5, y:p2.y - paddleH*0.5, width:paddleW, height:paddleH}, Color.white)
draw.rectangle({x:p1.x - paddleW*0.5, y:p1.y - paddleH*0.5, width:paddleW, height:paddleH}, color.white)
draw.rectangle({x:p2.x - paddleW*0.5, y:p2.y - paddleH*0.5, width:paddleW, height:paddleH}, color.white)
// Draw ball
draw.rectangle({x:ball.x - ball.size*0.5, y:ball.y - ball.size*0.5, width:ball.size, height:ball.size}, Color.white)
draw.rectangle({x:ball.x - ball.size*0.5, y:ball.y - ball.size*0.5, width:ball.size, height:ball.size}, color.white)
// Simple score display
var msg = score1 + " " + score2
draw.text(msg, {x:0, y:10, width:config.width, height:40}, undefined, 0, Color.white, 0)
draw.text(msg, {x:0, y:10, width:config.width, height:40}, undefined, 0, color.white, 0)
}

View File

@@ -4,6 +4,7 @@ var render = use('render')
var graphics = use('graphics')
var input = use('input')
var config = use('config')
var color = use('color')
prosperon.camera.transform.pos = [0,0]
@@ -83,15 +84,15 @@ this.hud = function() {
// Draw snake
for (var i=0; i<snake.length; i++) {
var s = snake[i]
draw.rectangle({x:s.x*cellSize, y:s.y*cellSize, width:cellSize, height:cellSize}, Color.green)
draw.rectangle({x:s.x*cellSize, y:s.y*cellSize, width:cellSize, height:cellSize}, color.green)
}
// Draw apple
draw.rectangle({x:apple.x*cellSize, y:apple.y*cellSize, width:cellSize, height:cellSize}, Color.red)
draw.rectangle({x:apple.x*cellSize, y:apple.y*cellSize, width:cellSize, height:cellSize}, color.red)
if (gameState === "gameover") {
var msg = "GAME OVER! Press SPACE to restart."
draw.text(msg, {x:0, y:config.height*0.5-10, width:config.width, height:20}, undefined, 0, Color.white)
draw.text(msg, {x:0, y:config.height*0.5-10, width:config.width, height:20}, undefined, 0, color.white)
}
}

View File

@@ -1,6 +1,7 @@
var draw = use('draw2d')
var input = use('input')
var config = use('config')
var color = use('color')
prosperon.camera.transform.pos = [0,0]
@@ -248,7 +249,7 @@ this.hud = function() {
}
// Next piece window
draw.text("Next", {x:70, y:5, width:50, height:10}, undefined, 0, Color.white)
draw.text("Next", {x:70, y:5, width:50, height:10}, undefined, 0, color.white)
if (nextPiece) {
for (var i=0; i<nextPiece.blocks.length; i++) {
var nx = nextPiece.blocks[i][0]
@@ -261,10 +262,10 @@ this.hud = function() {
// Score & Level
var info = "Score: " + score + "\nLines: " + linesCleared + "\nLevel: " + level
draw.text(info, {x:70, y:30, width:90, height:50}, undefined, 0, Color.white)
draw.text(info, {x:70, y:30, width:90, height:50}, undefined, 0, color.white)
if (gameOver) {
draw.text("GAME OVER", {x:10, y:config.height*0.5-5, width:config.width-20, height:20}, undefined, 0, Color.red)
draw.text("GAME OVER", {x:10, y:config.height*0.5-5, width:config.width-20, height:20}, undefined, 0, color.red)
}
}

View File

@@ -272,8 +272,6 @@ globalThis.use = function use(file, ...args) {
globalThis.json = use('json')
var time = use('time')
globalThis.mixin("color")
var DOCPATH = 'scripts/core/doc.js'
var script = io.slurp(DOCPATH)
var fnname = "doc"
@@ -706,7 +704,6 @@ function enet_check()
$_.delay(enet_check, service_delay);
}
send_messages();
enet_check();
// Finally, run the program

View File

@@ -5,17 +5,17 @@ function tohex(n) {
};
var Color = {
white: [255, 255, 255],
white: [1, 1, 1],
black: [0, 0, 0],
blue: [0, 0, 255],
green: [0, 255, 0],
yellow: [255, 255, 0],
red: [255, 0, 0],
gray: [181, 181, 181],
cyan: [0, 255, 255],
purple: [162, 93, 227],
orange: [255, 144, 64],
magenta: [255, 0, 255],
blue: [0, 0, 1],
green: [0, 1, 0],
yellow: [1, 1, 0],
red: [1, 0, 0],
gray: [0.71, 0.71, 0.71],
cyan: [0, 1, 1],
purple: [0.635, 0.365, 0.89],
orange: [1, 0.565, 0.251],
magenta: [1, 0, 1],
};
Color.editor = {};
@@ -41,85 +41,90 @@ esc.color = function (v) {
esc.doc = "Functions and constants for ANSI escape sequences.";
Color.Arkanoid = {
orange: [255, 143, 0],
teal: [0, 255, 255],
green: [0, 255, 0],
red: [255, 0, 0],
blue: [0, 112, 255],
purple: [255, 0, 255],
yellow: [255, 255, 0],
silver: [157, 157, 157],
gold: [188, 174, 0],
orange: [1, 0.561, 0],
teal: [0, 1, 1],
green: [0, 1, 0],
red: [1, 0, 0],
blue: [0, 0.439, 1],
purple: [1, 0, 1],
yellow: [1, 1, 0],
silver: [0.616, 0.616, 0.616],
gold: [0.737, 0.682, 0],
};
Color.Arkanoid.Powerups = {
red: [174, 0, 0] /* laser */,
blue: [0, 0, 174] /* enlarge */,
green: [0, 174, 0] /* catch */,
orange: [224, 143, 0] /* slow */,
purple: [210, 0, 210] /* break */,
cyan: [0, 174, 255] /* disruption */,
gray: [143, 143, 143] /* 1up */,
red: [0.682, 0, 0] /* laser */,
blue: [0, 0, 0.682] /* enlarge */,
green: [0, 0.682, 0] /* catch */,
orange: [0.878, 0.561, 0] /* slow */,
purple: [0.824, 0, 0.824] /* break */,
cyan: [0, 0.682, 1] /* disruption */,
gray: [0.561, 0.561, 0.561] /* 1up */,
};
Color.Gameboy = {
darkest: [229, 107, 26],
dark: [229, 189, 26],
light: [189, 229, 26],
lightest: [107, 229, 26],
darkest: [0.898, 0.42, 0.102],
dark: [0.898, 0.741, 0.102],
light: [0.741, 0.898, 0.102],
lightest: [0.42, 0.898, 0.102],
};
Color.Apple = {
green: [94, 189, 62],
yellow: [255, 185, 0],
orange: [247, 130, 0],
red: [226, 56, 56],
purple: [151, 57, 153],
blue: [0, 156, 223],
green: [0.369, 0.741, 0.243],
yellow: [1, 0.725, 0],
orange: [0.969, 0.51, 0],
red: [0.886, 0.22, 0.22],
purple: [0.592, 0.224, 0.6],
blue: [0, 0.612, 0.875],
};
Color.Debug = {
boundingbox: Color.white,
names: [84, 110, 255],
names: [0.329, 0.431, 1],
};
Color.Editor = {
grid: [99, 255, 128],
select: [255, 255, 55],
newgroup: [120, 255, 10],
grid: [0.388, 1, 0.502],
select: [1, 1, 0.216],
newgroup: [0.471, 1, 0.039],
};
/* Detects the format of all colors and munges them into a floating point format */
Color.normalize = function (c) {
var add_a = function (a) {
var n = this.slice();
n.a = a;
n[3] = a;
return n;
};
for (var p of Object.keys(c)) {
var fmt = "nrm";
if (typeof c[p] !== "object") continue;
if (!Array.isArray(c[p])) {
Color.normalize(c[p]);
continue;
}
c[p][3] = 255;
// Add alpha channel if not present
if (c[p].length === 3) {
c[p][3] = 1;
}
// Check if any values are > 1 (meaning they're in 0-255 format)
var needs_conversion = false;
for (var color of c[p]) {
if (color > 1) {
fmt = "8b";
needs_conversion = true;
break;
}
}
switch (fmt) {
case "8b":
c[p] = c[p].map(function (x) {
return x / 255;
});
// Convert from 0-255 to 0-1 if needed
if (needs_conversion) {
c[p] = c[p].map(function (x) {
return x / 255;
});
}
c[p].alpha = add_a;
}
};
@@ -133,53 +138,53 @@ ColorMap.makemap = function (map) {
return newmap;
};
ColorMap.Jet = ColorMap.makemap({
0: [0, 0, 131],
0.125: [0, 60, 170],
0.375: [5, 255, 255],
0.625: [255, 255, 0],
0.875: [250, 0, 0],
1: [128, 0, 0],
0: [0, 0, 0.514],
0.125: [0, 0.235, 0.667],
0.375: [0.02, 1, 1],
0.625: [1, 1, 0],
0.875: [0.98, 0, 0],
1: [0.502, 0, 0],
});
ColorMap.BlueRed = ColorMap.makemap({
0: [0, 0, 255],
1: [255, 0, 0],
0: [0, 0, 1],
1: [1, 0, 0],
});
ColorMap.Inferno = ColorMap.makemap({
0: [0, 0, 4],
0.13: [31, 12, 72],
0.25: [85, 15, 109],
0.38: [136, 34, 106],
0.5: [186, 54, 85],
0.63: [227, 89, 51],
0.75: [249, 140, 10],
0.88: [249, 201, 50],
1: [252, 255, 164],
0: [0, 0, 0.016],
0.13: [0.122, 0.047, 0.282],
0.25: [0.333, 0.059, 0.427],
0.38: [0.533, 0.133, 0.416],
0.5: [0.729, 0.212, 0.333],
0.63: [0.89, 0.349, 0.2],
0.75: [0.976, 0.549, 0.039],
0.88: [0.976, 0.788, 0.196],
1: [0.988, 1, 0.643],
});
ColorMap.Bathymetry = ColorMap.makemap({
0: [40, 26, 44],
0.13: [59.49, 90],
0.25: [64, 76, 139],
0.38: [63, 110, 151],
0.5: [72, 142, 158],
0.63: [85, 174, 163],
0.75: [120, 206, 163],
0.88: [187, 230, 172],
1: [253, 254, 204],
0: [0.157, 0.102, 0.173],
0.13: [0.233, 0.192, 0.353],
0.25: [0.251, 0.298, 0.545],
0.38: [0.247, 0.431, 0.592],
0.5: [0.282, 0.557, 0.62],
0.63: [0.333, 0.682, 0.639],
0.75: [0.471, 0.808, 0.639],
0.88: [0.733, 0.902, 0.675],
1: [0.992, 0.996, 0.8],
});
ColorMap.Viridis = ColorMap.makemap({
0: [68, 1, 84],
0.13: [71, 44, 122],
0.25: [59, 81, 139],
0.38: [44, 113, 142],
0.5: [33, 144, 141],
0.63: [39, 173, 129],
0.75: [92, 200, 99],
0.88: [170, 220, 50],
1: [253, 231, 37],
0: [0.267, 0.004, 0.329],
0.13: [0.278, 0.173, 0.478],
0.25: [0.231, 0.318, 0.545],
0.38: [0.173, 0.443, 0.557],
0.5: [0.129, 0.565, 0.553],
0.63: [0.153, 0.678, 0.506],
0.75: [0.361, 0.784, 0.388],
0.88: [0.667, 0.863, 0.196],
1: [0.992, 0.906, 0.145],
});
Color.normalize(ColorMap);
@@ -205,8 +210,7 @@ ColorMap.doc = {
sample: "Sample a given colormap at the given percentage (0 to 1).",
};
return {
Color,
esc,
ColorMap,
};
Color.maps = ColorMap
Color.utils = esc
return Color

View File

@@ -1,438 +1,87 @@
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 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.
A collection of 2D drawing functions that create drawing command lists.
These are pure functions that return plain JavaScript objects representing
drawing operations. No rendering or actor communication happens here.
`
// 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
// Create a new command list
draw.list = function() {
var commands = []
// 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
return {
// Add a command to this list
push: function(cmd) {
commands.push(cmd)
},
// Get all commands
get: function() {
return commands
},
// Clear all commands
clear: function() {
commands = []
},
// Get command count
length: function() {
return commands.length
}
})
// 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 = []
}
}
// Default command list for convenience
var current_list = draw.list()
// Set the current list
draw.set_list = function(list) {
current_list = list
}
// Get current list
draw.get_list = function() {
return current_list
}
// Clear current list
draw.clear = function() {
current_list.clear()
}
// Get commands from current list
draw.get_commands = function() {
return current_list.get()
}
// 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})
}
function add_command(type, data) {
var cmd = {cmd: type}
Object.assign(cmd, data)
current_list.push(cmd)
}
// Default geometry definitions
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,
@@ -441,145 +90,152 @@ var slice9_info = {
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)
var image_info = {
tile_x: false,
tile_y: false,
flip_x: false,
flip_y: false,
mode: 'linear'
}
// TODO: Implement slice9 rendering via SDL video actor
var circle_def = {
inner_radius:1, // percentage: 1 means filled circle
start:0,
end: 1,
}
// Drawing functions
draw.point = function(pos, size, opt = {}, material) {
add_command("draw_point", {
pos: pos,
size: size,
opt: opt,
material: material
})
}
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 opt: Optional geometry properties.
:param material: Material/styling information (color, shaders, etc.)
:return: None
`
draw.ellipse = function(pos, radii, def, material) {
var opt = def ? {...ellipse_def, ...def} : ellipse_def
if (opt.thickness <= 0) opt.thickness = Math.max(radii[0], radii[1])
add_command("draw_ellipse", {
pos: pos,
radii: radii,
opt: opt,
material: material
})
}
draw.line = function(points, def, material)
{
var opt = def ? {...line_def, ...def} : line_def
add_command("draw_line", {
points: points,
opt: opt,
material: material
})
}
draw.cross = function render_cross(pos, size, def, material) {
var a = [pos.add([0, size]), pos.add([0, -size])]
var b = [pos.add([size, 0]), pos.add([-size, 0])]
draw.line(a, def, material)
draw.line(b, def, material)
}
draw.arrow = function render_arrow(start, end, wingspan = 4, wingangle = 10, def, material) {
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, material)
draw.line(wing1, def, material)
draw.line(wing2, def, material)
}
draw.rectangle = function render_rectangle(rect, def, material) {
var opt = def ? {...rect_def, ...def} : rect_def
add_command("draw_rect", {
rect: rect,
opt: opt,
material: material
})
}
draw.slice9 = function slice9(image, rect = [0,0], slice = 0, info = slice9_info, material) {
if (!image) throw Error('Need an image to render.')
add_command("draw_slice9", {
image: image,
rect: rect,
slice: slice,
info: info,
material: material
})
}
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.
:param material: Material/styling information
: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) {
draw.image = function image(image, rect = [0,0], rotation = 0, anchor = [0,0], shear = [0,0], info = {}, material) {
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
rect = {x: rect[0], y: rect[1], width: 100, height: 100} // Default size
}
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
add_command("draw_image", {
image: image,
rect: rect,
rotation: rotation,
anchor: anchor,
shear: shear,
info: info,
material: material
})
}
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++
}
draw.circle = function render_circle(pos, radius, def, material) {
draw.ellipse(pos, [radius,radius], def, material)
}
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 = function text(text, rect, font = 'fonts/c64.ttf', size = 8, color = color.white, wrap = 0) {
add_command("draw_text", {
text: text,
rect: rect,
font: font,
size: size,
wrap: wrap,
material: {color}
})
}
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 size: Font size in pixels.
: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.
:param material: Material/styling information
:return: None
`

View File

@@ -1,4 +1,4 @@
var Color = use('color')
var color = use('color')
var os = use('os')
var graphics = use('graphics')
var transform = use('transform')
@@ -104,7 +104,7 @@ ex.scale = 1
ex.grow_for = 0
ex.spawn_timer = 0
ex.pps = 0
ex.color = Color.white
ex.color = color.white
ex.draw = function()
{

View File

@@ -1,4 +1,5 @@
var graphics = use('graphics')
var color = use('color')
var sprite = {
image: undefined,
@@ -223,7 +224,7 @@ return sprite;
---
var Color = use('color')
var color = use('color')
var transform = use('transform')
var sprite = use('sprite')
@@ -234,7 +235,7 @@ if (this.overling.transform)
this.transform.change_hook = $.t_hook;
var msp = new sprite
this._sprite = msp;
msp.color = Color.white;
msp.color = color.white;
this.transform.sprite = this

View File

@@ -1,5 +1,7 @@
var imgui = this;
var color = use('color')
var debug = {}
var imdebug = function imdebug() {

View File

@@ -0,0 +1,223 @@
/**
* Rasterization module for converting shapes to pixels/rects
* Used for software rendering of complex shapes
*/
var math = use('math')
var rasterize = {}
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)
if (start <= end) return t >= start && t <= end
return t >= start || t <= end
}
rasterize.ellipse = function(pos, radii, opt) {
opt = 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 || 0
var raw_end = opt.end || 1
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 || 1)
var rx_i = rx - thickness,
ry_i = ry - thickness
var hole = (rx_i > 0 && ry_i > 0)
if (!hole && thickness === 1) {
var points = []
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 add_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))
points = points.concat(pts)
}
while (px < py) {
add_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) {
add_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 {type: 'points', data: points}
}
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
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
}
}
}
return {type: 'rects', data: strips}
}
rasterize.circle = function(pos, radius, opt) {
return rasterize.ellipse(pos, [radius, radius], opt)
}
rasterize.outline_rect = function(rect, thickness) {
if (thickness <= 0) {
return {type: 'rect', data: rect}
}
if ((thickness << 1) >= rect.width ||
(thickness << 1) >= rect.height) {
return {type: 'rect', data: rect}
}
var x0 = rect.x,
y0 = rect.y,
x1 = rect.x + rect.width,
y1 = rect.y + rect.height
return {type: 'rects', data: [
{ x:x0, y:y0, width:rect.width, height:thickness },
{ x:x0, y:y1-thickness, width:rect.width, height:thickness },
{ x:x0, y:y0+thickness, width:thickness,
height:rect.height - (thickness<<1) },
{ x:x1-thickness, y:y0+thickness, width:thickness,
height:rect.height - (thickness<<1) }
]}
}
rasterize.round_rect = function(rect, radius, thickness) {
thickness = thickness || 1
if (thickness <= 0) {
return rasterize.fill_round_rect(rect, radius)
}
radius = Math.min(radius, rect.width >> 1, rect.height >> 1)
if ((thickness << 1) >= rect.width ||
(thickness << 1) >= rect.height ||
thickness >= radius) {
return rasterize.fill_round_rect(rect, radius)
}
var x0 = rect.x,
y0 = rect.y,
x1 = rect.x + rect.width - 1,
y1 = rect.y + rect.height - 1
var cx_l = x0 + radius, cx_r = x1 - radius
var cy_t = y0 + radius, cy_b = y1 - radius
var r_out = radius
var r_in = radius - thickness
var rects = [
{ x:x0 + radius, y:y0, width:rect.width - (radius << 1), height:thickness },
{ x:x0 + radius, y:y1 - thickness + 1, width:rect.width - (radius << 1), height:thickness },
{ x:x0, y:y0 + radius, width:thickness, height:rect.height - (radius << 1) },
{ x:x1 - thickness + 1, y:y0 + radius, width:thickness, height:rect.height - (radius << 1) }
]
var strips = []
for (var dy = 0; dy < radius; ++dy) {
var dy_sq = dy * dy
var dx_out = Math.floor(Math.sqrt(r_out * r_out - dy_sq))
var dx_in = (r_in > 0 && dy < r_in)
? Math.floor(Math.sqrt(r_in * r_in - dy_sq))
: -1
var w = dx_out - dx_in
if (w <= 0) continue
strips.push(
{ x:cx_l - dx_out, y:cy_t - dy, width:w, height:1 },
{ x:cx_r + dx_in + 1, y:cy_t - dy, width:w, height:1 },
{ x:cx_l - dx_out, y:cy_b + dy, width:w, height:1 },
{ x:cx_r + dx_in + 1, y:cy_b + dy, width:w, height:1 }
)
}
return {type: 'rects', data: rects.concat(strips)}
}
rasterize.fill_round_rect = function(rect, radius) {
radius = Math.min(radius, rect.width >> 1, rect.height >> 1)
var x0 = rect.x,
y0 = rect.y,
x1 = rect.x + rect.width - 1,
y1 = rect.y + rect.height - 1
var rects = [
{ x:x0 + radius, y:y0, width:rect.width - (radius << 1), height:rect.height },
{ 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) }
]
var cx_l = x0 + radius, cx_r = x1 - radius
var cy_t = y0 + radius, cy_b = y1 - radius
var caps = []
for (var dy = 0; dy < radius; ++dy) {
var dx = Math.floor(Math.sqrt(radius * radius - dy * dy))
var 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 }
)
}
return {type: 'rects', data: rects.concat(caps)}
}
return rasterize

View File

@@ -7,6 +7,7 @@ var tracy = use('tracy')
var graphics = use('graphics')
var imgui = use('imgui')
var transform = use('transform')
var color = use('color')
var base_pipeline = {
vertex: "sprite.vert",
@@ -666,7 +667,7 @@ function mask(image, pos, scale, rotation = 0, ref = 1) {
render.use_mat({
diffuse:image.texture,
rect: image.rect,
shade: Color.white
shade: color.white
});
render.draw(shape.quad);
}

View File

@@ -1,6 +1,7 @@
var render = use('render')
var os = use('os')
var transform = use('transform')
var color = use('color')
render.initialize({
width:500,
@@ -61,9 +62,9 @@ function loop()
draw.point([100,100])
draw.circle([200,200],40)
draw.ellipse([300,300],[20,40], {start:0,end:1, thickness:0})
draw.ellipse([350,350], [30,30], {start:0.1,end:-0.1, thickness:30, color: Color.yellow})
draw.ellipse([100,80],[40,25], {thickness:10, color:Color.green})
draw.ellipse([100,80], [40,25], {thickness:1,color:Color.blue})
draw.ellipse([350,350], [30,30], {start:0.1,end:-0.1, thickness:30, color: color.yellow})
draw.ellipse([100,80],[40,25], {thickness:10, color:color.green})
draw.ellipse([100,80], [40,25], {thickness:1,color:color.blue})
draw.rectangle({x:150,y:150,width:50,height:50})
draw.rectangle({x:100, y:60, width:200, height:60}, {radius: 20, thickness:-3})
draw.rectangle({x:350, y:60, width:200, height:120}, {radius:10,thickness:3})

View File

@@ -60,12 +60,7 @@ function start_drawing() {
var frame = 0;
var start_time = os.now();
// Load an image
var bunny_image = graphics.texture('tests/bunny.png')
send(video_actor, {kind: "cursor", op: "create", data: bunny_image.cpu}, ({id}) => {
send(video_actor, {kind:"cursor", op: "set", id})
})
// Note: Image loading would be handled by moth in real implementation
function draw_frame() {
frame++;
@@ -183,6 +178,7 @@ function start_drawing() {
draw2d.point(
[px, py],
3,
{},
{color: [1, 0.5 + Math.sin(t * 2 + i) * 0.5, 0.5, 1]}
);
}

220
tests/moth.js Normal file
View File

@@ -0,0 +1,220 @@
/**
* Moth Game Framework
* Higher-level game development framework built on top of Prosperon
*/
var os = use('os');
var io = use('io');
var transform = use('transform');
var rasterize = use('rasterize');
var video_actor = use('sdl_video')
var window
var render
var gameactor
$_.start(e => {
if (gameactor) return
gameactor = e.actor
loop()
}, 'tests/prosperon.js')
send(video_actor, {
kind: "window",
op:"create"
}, e => {
if (e.error) {
console.error(e.error)
os.exit(1)
}
window = e.id
send(video_actor,{
kind:"window",
op:"makeRenderer",
id:window
}, e => {
if (e.error) {
console.error(e.error)
os.exit(1)
}
render = e.id
console.log(`Created window and renderer id ${render}`)
})
})
var last = os.now()
// Engine state
var camera = {
x: 0,
y: 0,
scale: 1,
rotation: 0
}
// Convert high-level draw commands to low-level renderer commands
function translate_draw_commands(commands) {
var renderer_commands = []
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 "draw_rect":
// 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') {
// SDL video expects 'rects' operation, not 'fillRects'
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') {
// SDL video expects 'rects' operation with array
renderer_commands.push({
op: "rects",
data: {rects: raster_result.data}
})
}
} else {
// Filled rectangle
renderer_commands.push({
op: "fillRect",
data: {rect: cmd.rect}
})
}
break
case "draw_circle":
case "draw_ellipse":
// 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}
})
break
case "draw_point":
renderer_commands.push({
op: "point",
data: {points: [cmd.pos]}
})
break
case "draw_image":
// TODO: Handle image loading and texture management
renderer_commands.push({
op: "texture",
data: cmd
})
break
case "draw_text":
// Use debugText for now
renderer_commands.push({
op: "debugText",
data: {
pos: cmd.pos || {x: 0, y: 0},
text: cmd.text || ""
}
})
break
}
})
return renderer_commands
}
function loop()
{
var now = os.now()
var dt = now - last
last = now
// Update the game
send(gameactor, {kind:'update', dt:dt}, e => {
// Get draw commands from game
send(gameactor, {kind:'draw'}, draw_commands => {
var batch_commands = []
batch_commands.push({
op: "set",
prop: "drawColor",
value: [0.1,0.1,0.15,1]
})
// Clear the screen
batch_commands.push({
op: "clear"
})
if (draw_commands && draw_commands.length > 0) {
var renderer_commands = translate_draw_commands(draw_commands)
batch_commands = batch_commands.concat(renderer_commands)
}
batch_commands.push({
op: "present"
})
send(video_actor, {
kind: "renderer",
id: render,
op: "batch",
data: batch_commands
})
})
})
$_.delay(loop, 1/60)
}

62
tests/prosperon.js Normal file
View File

@@ -0,0 +1,62 @@
var draw2d = use('draw2d')
var color = use('color')
function update(dt)
{
// Update game logic here
return {}
}
function draw()
{
// Clear the draw list
draw2d.clear()
// Draw a red outlined rectangle
draw2d.rectangle(
{x: 100, y: 100, width: 200, height: 150}, // rect
{thickness: 3}, // geometry options
{color: color.red} // material
)
// Draw a filled green circle
draw2d.circle(
[300, 300], // position
50, // radius
{thickness: 0}, // geometry (0 = filled)
{color: color.green} // material
)
// Draw blue rounded rectangle
draw2d.rectangle(
{x: 350, y: 200, width: 150, height: 100},
{thickness: 2, radius: 15}, // rounded corners
{color: color.blue}
)
// Draw text
draw2d.text(
"Hello from Prosperon!",
{x: 50, y: 50},
'fonts/c64.ttf',
16,
color.white, // no wrap
)
// Draw a line
draw2d.line(
[[50, 400], [150, 450], [250, 400]],
{thickness: 2},
{color: color.yellow}
)
// Return the draw commands
return draw2d.get_commands()
}
$_.receiver(e => {
if (e.kind == 'update')
send(e, update(e.dt))
else if (e.kind == 'draw')
send(e, draw())
})