From b93a5a3ac0542e48b88553863cc08b639065bc2a Mon Sep 17 00:00:00 2001 From: John Alanbrook Date: Fri, 25 Apr 2025 17:56:17 -0500 Subject: [PATCH] refactor draw2d and render --- scripts/modules/draw2d.js | 498 ++++++++++++++++++++++++++-------- scripts/modules/graphics.js | 33 ++- scripts/modules/sdl_render.js | 249 ++--------------- source/jsffi.c | 48 ++-- tests/camera.js | 22 +- 5 files changed, 470 insertions(+), 380 deletions(-) diff --git a/scripts/modules/draw2d.js b/scripts/modules/draw2d.js index 7b5c2ee5..55408ebe 100644 --- a/scripts/modules/draw2d.js +++ b/scripts/modules/draw2d.js @@ -16,9 +16,14 @@ whiteimage.surface = graphics.make_surface([1,1]) whiteimage.surface.rect({x:0,y:0,width:1,height:1}, [1,1,1,1]) render.load_texture(whiteimage) -draw.point = function (pos, size, color = Color.blue) { - draw.rectangle({x:pos.x,y:pos.y, width:size,height:size}) -} +if (render.point) + draw.point = function(pos,size,opt = {color:Color.white}, pipeline) { + render.settings(opt) + render.pipeline(pipeline) + render.point([pos]) + } +else + draw.point = function() { throw new Error('Backend cannot draw points.') } draw.point[prosperon.DOC] = ` :param pos: A 2D position ([x, y]) where the point should be drawn. :param size: The size of the point. @@ -26,100 +31,347 @@ draw.point[prosperon.DOC] = ` :return: None ` -draw.line = function render_line(points, color = Color.white, thickness = 1, pipeline) { - var mesh = graphics.make_line_prim(points, thickness, 0, 0, color) - render.queue({ - type: 'geometry', - mesh, - pipeline, - first_index: 0, - num_indices: mesh.num_indices, - image:whiteimage - }) -} -draw.line[prosperon.DOC] = ` -:param points: An array of 2D positions representing the line vertices. -:param color: The color of the line, default Color.white. -:param thickness: The line thickness, default 1. -:param pipeline: (Optional) A pipeline or rendering state object. -:return: None -` +/* ------------------------------------------------------------------------ + * helper – is (dx,dy) inside the desired wedge? + * -------------------------------------------------------------------- */ +function within_wedge(dx, dy, start, end, full_circle) +{ + if (full_circle) return true -draw.cross = function render_cross(pos, size, color = Color.red, thickness = 1, pipe) { + var ang = Math.atan2(dy, dx) // [-π,π] + if (ang < 0) ang += Math.PI * 2 + var t = ang / (Math.PI * 2) // turn ∈ [0,1) + + if (start <= end) return t >= start && t <= end + return t >= start || t <= end // wrap-around arc +} + +/* ------------------------------------------------------------------------ + * software ellipse – outline / ring / fill via inner_radius + * -------------------------------------------------------------------- */ +function software_ellipse(pos, radii, opt) +{ + var rx = radii[0], ry = radii[1] + if (rx <= 0 || ry <= 0) return + + var cx = pos[0], cy = pos[1] + var raw_start = opt.start + var raw_end = opt.end + var full_circle = Math.abs(raw_end - raw_start) >= 1 - 1e-9 + var start = (raw_start % 1 + 1) % 1 + var end = (raw_end % 1 + 1) % 1 + var thickness = Math.max(1, opt.thickness|0) + + /* inner ellipse radii (may be zero or negative → treat as no hole) */ + var rx_i = rx - thickness, + ry_i = ry - thickness + var hole = (rx_i > 0 && ry_i > 0) + + /* fast one-pixel outline ? --------------------------------------- */ + if (!hole && thickness === 1) { + /* (same midpoint algorithm as before, filtered by wedge) --------*/ + var rx_sq = rx * rx, ry_sq = ry * ry + var two_rx_sq = rx_sq << 1, two_ry_sq = ry_sq << 1 + var x = 0, y = ry, px = 0, py = two_rx_sq * y + var p = ry_sq - rx_sq * ry + 0.25 * rx_sq + + function plot_pts(x, y) { + var pts = [ + [cx + x, cy + y], [cx - x, cy + y], + [cx + x, cy - y], [cx - x, cy - y] + ].filter(pt => within_wedge(pt[0]-cx, pt[1]-cy, start, end, full_circle)) + if (pts.length) render.point(pts) + } + + while (px < py) { + plot_pts(x, y) + ++x; px += two_ry_sq + if (p < 0) p += ry_sq + px + else { --y; py -= two_rx_sq; p += ry_sq + px - py } + } + p = ry_sq*(x+.5)*(x+.5) + rx_sq*(y-1)*(y-1) - rx_sq*ry_sq + while (y >= 0) { + plot_pts(x, y) + --y; py -= two_rx_sq + if (p > 0) p += rx_sq - py + else { ++x; px += two_ry_sq; p += rx_sq - py + px } + } + return + } + + /* ------------------------------------------------------------------ + * FILL or RING (thickness ≥ 2 OR hole == false -> full fill) + * ---------------------------------------------------------------- */ + var strips = [] + var rx_sq = rx * rx, ry_sq = ry * ry + var rx_i_sq = rx_i * rx_i, ry_i_sq = ry_i * ry_i + + for (var dy = -ry; dy <= ry; ++dy) { + var yy = dy * dy + var x_out = Math.floor(rx * Math.sqrt(1 - yy / ry_sq)) + var y_screen = cy + dy + + /* optional inner span */ + var x_in = hole ? Math.floor(rx_i * Math.sqrt(1 - yy / ry_i_sq)) : -1 + + var run_start = null + for (var dx = -x_out; dx <= x_out; ++dx) { + if (hole && Math.abs(dx) <= x_in) { run_start = null; continue } + if (!within_wedge(dx, dy, start, end, full_circle)) { run_start = null; continue } + + if (run_start === null) run_start = cx + dx + + var last = (dx === x_out) + var next_in_ring = + !last && + !(hole && Math.abs(dx+1) <= x_in) && + within_wedge(dx+1, dy, start, end, full_circle) + + if (last || !next_in_ring) { + strips.push({ + x: run_start, + y: y_screen, + width: (cx + dx) - run_start + 1, + height: 1 + }) + run_start = null + } + } + } + if (strips.length) render.rects(strips) +} + +var ellipse_def = { + color: Color.white, + start: 0, + end: 1, + mode: 'fill', + thickness: 1, +} + +if (render.ellipse) + draw.ellipse = function(pos, radii, def, pipeline) { + } +else + draw.ellipse = function(pos, radii, def, pipeline) { + var opt = def ? {...ellipse_def, ...def} : ellipse_def + render.settings(opt) + render.pipeline(pipeline) + if (opt.thickness <= 0) opt.thickness = Math.max(radii[0], radii[1]) + software_ellipse(pos, radii, opt) + } + +var line_def = { + color: Color.white, + thickness: 1, + cap:"butt", +} + +draw.line = function(points, def, pipeline) +{ + var opt + if (def) + opt = {...line_def, ...def} + else + opt = line_def + + render.settings(opt) + render.pipeline(pipeline) + render.line(points) +} + +draw.cross = function render_cross(pos, size, def, pipe) { var a = [pos.add([0, size]), pos.add([0, -size])] var b = [pos.add([size, 0]), pos.add([-size, 0])] - draw.line(a, color, thickness) - draw.line(b, color, thickness) + draw.line(a, def, pipe) + draw.line(b, def,pipe) } -draw.cross[prosperon.DOC] = ` -:param pos: The center of the cross as a 2D position ([x, y]). -:param size: Half the size of each cross arm. -:param color: The color of the cross, default Color.red. -:param thickness: The thickness of each line, default 1. -:param pipe: (Optional) A pipeline or rendering state object. -:return: None -` -draw.arrow = function render_arrow(start, end, color = Color.red, wingspan = 4, wingangle = 10, 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], color) - draw.line(wing1, color) - draw.line(wing2, color) + draw.line([start, end], def, pipe) + draw.line(wing1, def, pipe) + draw.line(wing2, def, pipe) } -draw.arrow[prosperon.DOC] = ` -:param start: The start position of the arrow ([x, y]). -:param end: The end (tip) position of the arrow ([x, y]). -:param color: The color, default Color.red. -:param wingspan: The length of each arrowhead 'wing', default 4. -:param wingangle: Wing rotation in degrees, default 10. -:param pipe: (Optional) A pipeline or rendering state object. -:return: None -` -draw.rectangle = function render_rectangle(rect, color = Color.white, pipeline) { - render.queue({ - type: 'rectangle', - material: { - color, - }, - rect, - pipeline, - }) +/* ------------------------------------------------------------------------ + * helper – plain rectangle outline of arbitrary thickness (radius=0) + * -------------------------------------------------------------------- */ +function software_outline_rect(rect, thickness) +{ + if (thickness <= 0) { + render.rectangle(rect); + return; + } + + /* stroke swallows the whole thing → fill instead */ + if ((thickness << 1) >= rect.width || + (thickness << 1) >= rect.height) { + render.rectangle(rect) // filled + return + } + + const x0 = rect.x, + y0 = rect.y, + x1 = rect.x + rect.width, + y1 = rect.y + rect.height + + render.rects([ + { x:x0, y:y0, width:rect.width, height:thickness }, // top + { x:x0, y:y1-thickness, width:rect.width, height:thickness }, // bottom + { x:x0, y:y0+thickness, width:thickness, + height:rect.height - (thickness<<1) }, // left + { x:x1-thickness, y:y0+thickness, width:thickness, + height:rect.height - (thickness<<1) } // right + ]) } -draw.rectangle[prosperon.DOC] = ` -:param rect: A rectangle object with {x, y, width, height}. -:param color: The fill color, default Color.white. -:param pipeline: (Optional) A pipeline or rendering state object. -:return: None -` -var tile_def = {repeat_x:true, repeat_y:true} +/* ------------------------------------------------------------------------ + * 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 + } -draw.tile = function(image, rect, color = Color.white, tile = tile_def, pipeline) { - if (!image) throw Error('Need an image to render.') - if (typeof image === "string") - image = graphics.texture(image) - var mesh = geometry.tile(image.texture, {x:0,y:0,width:image.texture.width,height:image.texture.height}, rect, tile) - render.queue({ - type:'geometry', - mesh, - image, - pipeline, - first_index:0, - num_indices:mesh.num_indices - }) + radius = Math.min(radius, rect.width >> 1, rect.height >> 1) + + /* stroke covers whole rect → fall back to fill ------------------- */ + if ((thickness << 1) >= rect.width || + (thickness << 1) >= rect.height || + thickness >= radius) { + software_fill_round_rect(rect, radius) + return + } + + const x0 = rect.x, + y0 = rect.y, + x1 = rect.x + rect.width - 1, // inclusive + y1 = rect.y + rect.height - 1 + + const cx_l = x0 + radius, cx_r = x1 - radius + const cy_t = y0 + radius, cy_b = y1 - radius + const r_out = radius + const r_in = radius - thickness + + /* straight bands (top/bottom/left/right) ------------------------- */ + render.rects([ + { x:x0 + radius, y:y0, width:rect.width - (radius << 1), + height:thickness }, // top + { x:x0 + radius, y:y1 - thickness + 1, + width:rect.width - (radius << 1), height:thickness }, // bottom + { x:x0, y:y0 + radius, width:thickness, + height:rect.height - (radius << 1) }, // left + { x:x1 - thickness + 1, y:y0 + radius, width:thickness, + height:rect.height - (radius << 1) } // right + ]) + + /* corner arcs ---------------------------------------------------- */ + const strips = [] + + for (let dy = 0; dy < radius; ++dy) { + const dy_sq = dy * dy + const dx_out = Math.floor(Math.sqrt(r_out * r_out - dy_sq)) + const dx_in = (r_in > 0 && dy < r_in) + ? Math.floor(Math.sqrt(r_in * r_in - dy_sq)) + : -1 // no inner rim + const w = dx_out - dx_in // strip width + if (w <= 0) continue + + /* top */ + strips.push( + { x:cx_l - dx_out, y:cy_t - dy, width:w, height:1 }, // NW + { x:cx_r + dx_in + 1, y:cy_t - dy, width:w, height:1 } // NE + ) + + /* bottom */ + strips.push( + { x:cx_l - dx_out, y:cy_b + dy, width:w, height:1 }, // SW + { x:cx_r + dx_in + 1, y:cy_b + dy, width:w, height:1 } // SE + ) + } + + render.rects(strips) +} + +/* ------------------------------------------------------------------------ + * filled rounded rect (unchanged) + * -------------------------------------------------------------------- */ +function software_fill_round_rect(rect, radius) +{ + radius = Math.min(radius, rect.width >> 1, rect.height >> 1) + + const x0 = rect.x, + y0 = rect.y, + x1 = rect.x + rect.width - 1, + y1 = rect.y + rect.height - 1 + + /* main column */ + render.rects([ + { x:x0 + radius, y:y0, width:rect.width - (radius << 1), + height:rect.height } + ]) + + /* side columns */ + render.rects([ + { x:x0, y:y0 + radius, width:radius, + height:rect.height - (radius << 1) }, + { x:x1 - radius + 1, y:y0 + radius, width:radius, + height:rect.height - (radius << 1) } + ]) + + /* corner caps */ + const cx_l = x0 + radius, cx_r = x1 - radius + const cy_t = y0 + radius, cy_b = y1 - radius + const caps = [] + + for (let dy = 0; dy < radius; ++dy) { + const dx = Math.floor(Math.sqrt(radius * radius - dy * dy)) + const w = (dx << 1) + 1 + + caps.push( + { x:cx_l - dx, y:cy_t - dy, width:w, height:1 }, + { x:cx_r - dx, y:cy_t - dy, width:w, height:1 }, + { x:cx_l - dx, y:cy_b + dy, width:w, height:1 }, + { x:cx_r - dx, y:cy_b + dy, width:w, height:1 } + ) + } + + render.rects(caps) +} + +var rect_def = { + thickness:1, + mode: 'fill', + color: Color.white, + radius: 0, + thickness:1 +} +draw.rectangle = function render_rectangle(rect, def, pipeline) { + var opt = def ? {...rect_def, ...def} : rect_def + render.settings(opt) + render.pipeline(pipeline) + + var t = opt.thickness|0 + + if (t <= 0) { + if (opt.radius) + software_fill_round_rect(rect, opt.radius) + else + render.rectangle(rect) + + return + } + + if (opt.radius) + software_round_rect(rect, opt.radius, t) + else + software_outline_rect(rect,t) } -draw.tile[prosperon.DOC] = ` -:param image: An image object or string path to a texture. -:param rect: A rectangle specifying draw location/size ({x, y, width, height}). -:param color: The color tint, default Color.white. -:param tile: A tiling definition ({repeat_x, repeat_y}), default tile_def. -:param pipeline: (Optional) A pipeline or rendering state object. -:return: None -:raises Error: If no image is provided. -` var slice9_info = { tile_top:true, @@ -127,10 +379,11 @@ var slice9_info = { tile_left:true, tile_right:true, tile_center_x:true, - tile_center_right:true + tile_center_right:true, + color: Color.white, } -draw.slice9 = function slice9(image, rect = [0,0], slice = 0, color = Color.white, info = slice9_info, pipeline) { +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) @@ -148,7 +401,6 @@ 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 color: The color tint, default Color.white. :param info: A slice9 info object controlling tiling of edges/corners. :param pipeline: (Optional) A pipeline or rendering state object. :return: None @@ -156,33 +408,24 @@ draw.slice9[prosperon.DOC] = ` ` var std_sprite_cmd = {type:'sprite', color:[1,1,1,1]} +var image_info = { + tile_x: false, + tile_y: false, + flip_x: false, + flip_y: false, + color: Color.white, +} -draw.image = function image(image, rect = [0,0], rotation = 0, color, pipeline) { +draw.image = function image(image, rect = [0,0], rotation = 0, anchor = [0,0], shear = [0,0], info, pipeline) { if (!image) throw Error('Need an image to render.') if (typeof image === "string") image = graphics.texture(image) rect.width ??= image.texture.width rect.height ??= image.texture.height - var cmd = Object.create(std_sprite_cmd) - cmd.image = image - cmd.rect = rect - if (pipeline) cmd.pipeline = pipeline - if (color) cmd.color = color - render.queue(cmd) - var sprite = graphics.make_sprite() - sprite.set_image(image) - sprite.set_rect(rect) - return sprite + info ??= image_info; + render.settings(info) + render.image(image, rect, rotation, anchor, shear, info) } -draw.image[prosperon.DOC] = ` -:param image: An image object or string path to a texture. -:param rect: A rectangle specifying draw location/size, default [0,0]; width/height default to image size. -:param rotation: Rotation in degrees (not currently used). -:param color: The color tint, default none. -:param pipeline: (Optional) A pipeline or rendering state object. -:return: A sprite object that was created for this draw call. -:raises Error: If no image is provided. -` draw.images = function images(image, rects, config) { if (!image) throw Error('Need an image to render.') @@ -223,17 +466,40 @@ draw.sprites[prosperon.DOC] = ` :return: None ` -draw.circle = function render_circle(pos, radius, color, inner_radius = 1, pipeline) { - draw.rectangle({x:pos.x, y:pos.y, width:radius*2,height:radius*2}, color, circle_pipeline) +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) } -draw.circle[prosperon.DOC] = ` -:param pos: Center of the circle ([x, y]). -:param radius: The circle radius. -:param color: The fill color of the circle, default none. -:param inner_radius: (Unused) Possibly ring thickness, default 1. -:param pipeline: (Optional) A pipeline or rendering state object. -:return: None -` var sysfont = graphics.get_font('fonts/c64.ttf', 8) diff --git a/scripts/modules/graphics.js b/scripts/modules/graphics.js index b218e9df..fd9304a9 100644 --- a/scripts/modules/graphics.js +++ b/scripts/modules/graphics.js @@ -15,6 +15,22 @@ function calc_image_size(img) { return [img.texture.width * img.rect.width, img.texture.height * img.rect.height] } +function decorate_rect_px(img) { + // needs a GPU texture to measure + if (!img || !img.texture) return + + // default UV rect is the whole image if none supplied + img.rect ??= {x:0, y:0, width:1, height:1} // [u0,v0,uw,vh] in 0-1 + + // store pixel-space version: [x, y, w, h] in texels + img.rect_px = { + x:Math.round(img.rect.x * img.texture.width), + y:Math.round(img.rect.y * img.texture.height), + width:Math.round(img.rect.width * img.texture.width), + height:Math.round(img.rect.height * img.texture.height) + } +} + /** Internally loads image data from disk and prepares a GPU texture. Used by graphics.texture(). Not intended for direct user calls. @@ -27,24 +43,32 @@ function create_image(path) { switch (path.ext()) { case 'gif': newimg = graphics.make_gif(data); - if (newimg.surface) + if (newimg.surface) { render.load_texture(newimg) + decorate_rect_px(newimg) + } else { - for (var frame of newimg.frames) + for (var frame of newimg.frames) { render.load_texture(frame) + decorate_rect_px(frame) + } } break; case 'ase': case 'aseprite': newimg = graphics.make_aseprite(data); - if (newimg.surface) + if (newimg.surface) { render.load_texture(newimg) + decorate_rect_px(newimg) + } else { for (var anim in newimg) { var a = newimg[anim]; - for (var frame of a.frames) + for (var frame of a.frames) { render.load_texture(frame) + decorate_rect_px(frame) + } } } break; @@ -54,6 +78,7 @@ function create_image(path) { surface: graphics.make_texture(data) }; render.load_texture(newimg) + decorate_rect_px(newimg) break; } diff --git a/scripts/modules/sdl_render.js b/scripts/modules/sdl_render.js index fb5482d2..a5d75026 100644 --- a/scripts/modules/sdl_render.js +++ b/scripts/modules/sdl_render.js @@ -34,6 +34,12 @@ render.initialize = function(config) context = prosperon.window.make_renderer() } +render.sprite = function(sprite) +{ + + context.sprite(sprite) +} + // img here is the engine surface render.load_texture = function(img) { @@ -48,15 +54,11 @@ render.load_texture = function(img) var current_color = Color.white -render.image = function(image, rect) +render.image = function(image, rect, rotation, anchor, shear, info) { - if (typeof image === 'string') - image = graphics.texture(image) - - rect.width ??= image.texture.width - rect.height ??= image.texture.height - var T = - context.texture(image.texture, rect); + rect.width = image.rect_px.width; + rect.height = image.rect_px.height; + context.texture(image.texture, image.rect_px, rect, rotation * 360, anchor); } render.clip = function(rect) @@ -64,228 +66,37 @@ render.clip = function(rect) context.clip(rect) } -render.circle = function(pos, radius, color = Color.white) +render.line = function(points) { - 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 - - context.draw_color(color) - - while (x <= y) { - context.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++ - } -} - -/*------------------------------------------------------------* - * Rounded-rect outline – any stroke width ≥ 1 * - *------------------------------------------------------------*/ -render.round_rect = function(rect, radius, line_width = 1, - color = Color.white) -{ - if (line_width <= 0) return - - radius = Math.min(radius, rect.width >> 1, rect.height >> 1) - if (radius <= 0) { render.rectangle(rect, color); return } - - const x0 = rect.x, - y0 = rect.y, - x1 = rect.x + rect.width - 1, // inclusive - y1 = rect.y + rect.height - 1 - - /* fast-path: stroke swallows the whole thing → just fill */ - if ((line_width << 1) >= rect.width || - (line_width << 1) >= rect.height) { - render.fill_round_rect(rect, radius, color) - return - } - - 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 = Math.max(radius - line_width, 0) - - context.draw_color(color) - - /* straight bands ---------------------------------------------------------*/ - context.rects([ - { x:x0 + radius, y:y0, width:rect.width - (radius << 1), - height:line_width }, // top - { x:x0 + radius, y:y1 - line_width + 1, - width:rect.width - (radius << 1), height:line_width }, // bottom - { x:x0, y:y0 + radius, width:line_width, - height:rect.height - (radius << 1) }, // left - { x:x1 - line_width + 1, y:y0 + radius, width:line_width, - height:rect.height - (radius << 1) } // right - ]) - - /* corner arcs ------------------------------------------------------------*/ - const strips = [] // batch for speed - - 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 && 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 - ) - } - - context.rects(strips) -} - -/*------------------------------------------------------------* - * Filled rounded-rect * - *------------------------------------------------------------*/ -render.fill_round_rect = function(rect, radius, - color = Color.white) -{ - radius = Math.min(radius, rect.width >> 1, rect.height >> 1) - if (radius <= 0) { context.draw_color(color); context.rects([rect]); return } - - const x0 = rect.x, - y0 = rect.y, - x1 = rect.x + rect.width - 1, - y1 = rect.y + rect.height - 1 - - context.draw_color(color) - - /* main rectangle column (everything between the caps) ----*/ - context.rects([ - { x:x0 + radius, y:y0, width:rect.width - (radius << 1), - height:rect.height } - ]) - - /* side columns (left & right) -----------------------------*/ - context.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 - - /* top */ - 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 } - ) - - /* bottom */ - caps.push( - { x:cx_l - dx, y:cy_b + dy, width:w, height:1 }, - { x:cx_r - dx, y:cy_b + dy, width:w, height:1 } - ) - } - - context.rects(caps) -} - -render.ellipse = function(pos, radiuses, color = Color.white) -{ - var rx = radiuses[0], ry = radiuses[1] - if (rx <= 0 || ry <= 0) return // nothing to draw - - var cx = pos[0], cy = pos[1] - - 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 - var px = 0, py = two_rx_sq * y - - context.draw_color(color) - - // Region 1 - var p = ry_sq - rx_sq * ry + (0.25 * rx_sq) - while (px < py) { - context.point([ - [cx + x, cy + y], [cx - x, cy + y], - [cx + x, cy - y], [cx - x, cy - y] - ]) - - x++ - px += two_ry_sq - if (p < 0) p += ry_sq + px - else { - y-- - py -= two_rx_sq - p += ry_sq + px - py - } - } - - // Region 2 - p = ry_sq * (x + 0.5) * (x + 0.5) + rx_sq * (y - 1) * (y - 1) - rx_sq * ry_sq - while (y >= 0) { - context.point([ - [cx + x, cy + y], [cx - x, cy + y], - [cx + x, cy - y], [cx - x, cy - y] - ]) - - y-- - py -= two_rx_sq - if (p > 0) p += rx_sq - py - else { - x++ - px += two_ry_sq - p += rx_sq - py + px - } - } -} - -render.line = function(points, color = Color.white) -{ - context.draw_color(color) context.line(points) } -render.point = function(pos, color = Color.white) +render.point = function(pos) { - context.draw_color(color) - context.point([pos]) + context.point(pos) } -render.rectangle = function(rect, color = Color.white) +render.rectangle = function(rect) { - context.draw_color(color) context.rects([rect]) } +render.rects = function(rects) +{ + context.rects(rects) +} + +render.pipeline = function(pipe) +{ + // any changes here +} + +render.settings = function(set) +{ + if (!set.color) return + context.draw_color(set.color) +} + render.geometry = function(mesh, pipeline) { diff --git a/source/jsffi.c b/source/jsffi.c index 360a0296..8c03dab2 100644 --- a/source/jsffi.c +++ b/source/jsffi.c @@ -3056,39 +3056,26 @@ JSC_CCALL(renderer_texture_9grid, JSC_CCALL(renderer_texture, renderer_ctx *ctx = js2renderer_ctx(js, self); - SDL_Renderer *r = ctx->sdl; - - SDL_Texture *tex = js2SDL_Texture(js, argv[0]); + SDL_Texture *tex = js2SDL_Texture(js,argv[0]); rect src = js2rect(js,argv[1]); - transform *xf = js2transform (js, argv[2]); - HMM_Vec2 k = js2vec2 (js, argv[3]); /* default (0,0) */ + rect dst = js2rect(js,argv[2]); + double angle; + JS_ToFloat64(js, &angle, argv[3]); + HMM_Vec2 anchor = js2vec2(js, argv[4]); + SDL_RenderTextureRotated(ctx->sdl, tex, &src, &dst, angle, &anchor, SDL_FLIP_NONE); +) - HMM_Mat3 m3 = transform2mat3(xf); /* includes rot & scale */ +JSC_CCALL(renderer_sprite, + renderer_ctx *ctx = js2renderer_ctx(js, self); + sprite *sp = js2sprite(js,argv[0]); - HMM_Vec2 right = mat_right(m3); /* local +X in world */ - HMM_Vec2 up = mat_up (m3); /* local +Y in world (Y-up)*/ + SDL_Texture *tex; + JS_GETATOM(js, tex, sp->image, texture, SDL_Texture); - HMM_Vec2 r2 = HMM_AddV2(right, HMM_MulV2F(up, k.x)); - HMM_Vec2 u2 = HMM_AddV2(up, HMM_MulV2F(right,k.y)); - - r2 = HMM_MulV2F(r2, src.w); - u2 = HMM_MulV2F(u2, src.h); - - HMM_Vec2 origin_w = { xf->pos.x, xf->pos.y }; - HMM_Vec2 right_w = HMM_AddV2(origin_w, r2); - HMM_Vec2 down_w = HMM_AddV2(origin_w, u2); /* SDL wants “down” */ - - SDL_FPoint origin = renderer_world_to_screen(ctx, origin_w); - SDL_FPoint rightp = renderer_world_to_screen(ctx, right_w); - SDL_FPoint downp = renderer_world_to_screen(ctx, down_w); - - if (!SDL_RenderTextureAffine(r, tex, - &src, /* full image */ - &origin, - &rightp, - &downp)) - return JS_ThrowReferenceError(js, - "SDL_RenderTextureAffine: %s", SDL_GetError()); +// rect dst = renderer_worldrect_to_screen(ctx, sp->affine); + rect dst = sp->affine; + rect uv = {x:0,y:0,w:48,h:24}; + SDL_RenderTexture(ctx->sdl, tex, &uv, &dst); ) static const JSCFunctionListEntry js_renderer_ctx_funcs[] = { @@ -3097,9 +3084,10 @@ static const JSCFunctionListEntry js_renderer_ctx_funcs[] = { MIST_FUNC_DEF(SDL_Renderer, clear, 0), MIST_FUNC_DEF(renderer, line, 1), MIST_FUNC_DEF(renderer, point, 1), - MIST_FUNC_DEF(renderer, texture, 2), + MIST_FUNC_DEF(renderer, texture, 5), MIST_FUNC_DEF(renderer, rects, 1), MIST_FUNC_DEF(renderer, geometry, 2), + MIST_FUNC_DEF(renderer, sprite, 1), MIST_FUNC_DEF(renderer, load_texture, 1), MIST_FUNC_DEF(renderer, get_image, 1), diff --git a/tests/camera.js b/tests/camera.js index c9b77bae..c44a8702 100644 --- a/tests/camera.js +++ b/tests/camera.js @@ -39,19 +39,19 @@ function loop() { render.clear(Color.red) render.camera(hudcam) - render.line([[0,0],[100,50]]) - render.point([100,100]) - render.circle([200,200],40) - render.ellipse([300,300],[20,40]) - render.rectangle({x:150,y:150,width:50,height:50}) - render.round_rect({x:100, y:60, width:200, height:60}, 20, 2) - render.fill_round_rect({x:350, y:60, width:200, height:120}, 10) - //render.image("button_grey", [100,100]) -// draw.rectangle({x:50,y:-50,width:50,height:50}) +/* draw.line([[0,0],[100,50]]) + 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.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})*/ + draw.image("button_grey", [100,100],0.25) render.present() $_.delay(loop, 1/60) } loop() - -