From 01df337ccc24b99dfe52b2ad72db7b023daf0661 Mon Sep 17 00:00:00 2001 From: John Alanbrook Date: Tue, 27 May 2025 13:46:56 -0500 Subject: [PATCH] draw2d now generates high level commands; turned into instructions by moth --- examples/bunnymark/main.js | 3 +- examples/pong/main.js | 9 +- examples/snake/main.js | 7 +- examples/tetris/main.js | 7 +- scripts/core/engine.js | 3 - scripts/modules/color.js | 182 ++++----- scripts/modules/draw2d.js | 682 ++++++++------------------------- scripts/modules/ext/emitter.js | 4 +- scripts/modules/ext/sprite.js | 5 +- scripts/modules/imgui.js | 2 + scripts/modules/rasterize.js | 223 +++++++++++ scripts/modules/sdl_gpu.js | 3 +- tests/camera.js | 7 +- tests/draw2d.js | 8 +- tests/moth.js | 220 +++++++++++ tests/prosperon.js | 62 +++ 16 files changed, 797 insertions(+), 630 deletions(-) create mode 100644 scripts/modules/rasterize.js create mode 100644 tests/moth.js create mode 100644 tests/prosperon.js diff --git a/examples/bunnymark/main.js b/examples/bunnymark/main.js index ec98310b..31ee372b 100644 --- a/examples/bunnymark/main.js +++ b/examples/bunnymark/main.js @@ -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) } diff --git a/examples/pong/main.js b/examples/pong/main.js index 63a6a52a..768852d7 100644 --- a/examples/pong/main.js +++ b/examples/pong/main.js @@ -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) } diff --git a/examples/snake/main.js b/examples/snake/main.js index f4562192..f40c965c 100644 --- a/examples/snake/main.js +++ b/examples/snake/main.js @@ -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 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 diff --git a/scripts/modules/draw2d.js b/scripts/modules/draw2d.js index 8f53d523..3eae7fa1 100644 --- a/scripts/modules/draw2d.js +++ b/scripts/modules/draw2d.js @@ -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 ` diff --git a/scripts/modules/ext/emitter.js b/scripts/modules/ext/emitter.js index 44684f22..b8fafce3 100644 --- a/scripts/modules/ext/emitter.js +++ b/scripts/modules/ext/emitter.js @@ -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() { diff --git a/scripts/modules/ext/sprite.js b/scripts/modules/ext/sprite.js index 5ade0f43..627c4a22 100644 --- a/scripts/modules/ext/sprite.js +++ b/scripts/modules/ext/sprite.js @@ -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 diff --git a/scripts/modules/imgui.js b/scripts/modules/imgui.js index 3fd5edc5..c1983d9f 100644 --- a/scripts/modules/imgui.js +++ b/scripts/modules/imgui.js @@ -1,5 +1,7 @@ var imgui = this; +var color = use('color') + var debug = {} var imdebug = function imdebug() { diff --git a/scripts/modules/rasterize.js b/scripts/modules/rasterize.js new file mode 100644 index 00000000..e69b4efa --- /dev/null +++ b/scripts/modules/rasterize.js @@ -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 \ No newline at end of file diff --git a/scripts/modules/sdl_gpu.js b/scripts/modules/sdl_gpu.js index 77f078e1..d83843a0 100644 --- a/scripts/modules/sdl_gpu.js +++ b/scripts/modules/sdl_gpu.js @@ -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); } diff --git a/tests/camera.js b/tests/camera.js index e9cba186..1d97c4fe 100644 --- a/tests/camera.js +++ b/tests/camera.js @@ -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}) diff --git a/tests/draw2d.js b/tests/draw2d.js index 8b01ee66..4340d906 100644 --- a/tests/draw2d.js +++ b/tests/draw2d.js @@ -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]} ); } diff --git a/tests/moth.js b/tests/moth.js new file mode 100644 index 00000000..ab35ff75 --- /dev/null +++ b/tests/moth.js @@ -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) +} diff --git a/tests/prosperon.js b/tests/prosperon.js new file mode 100644 index 00000000..05ec9812 --- /dev/null +++ b/tests/prosperon.js @@ -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()) +}) \ No newline at end of file