draw textures with draw2d
Some checks failed
Build and Deploy / build-macos (push) Failing after 6s
Build and Deploy / build-windows (CLANG64) (push) Has been cancelled
Build and Deploy / package-dist (push) Has been cancelled
Build and Deploy / deploy-itch (push) Has been cancelled
Build and Deploy / deploy-gitea (push) Has been cancelled
Build and Deploy / build-linux (push) Has been cancelled

This commit is contained in:
2025-05-26 13:25:56 -05:00
parent 1b97527120
commit af21e10e97
8 changed files with 403 additions and 193 deletions

View File

@@ -348,6 +348,22 @@ function handle_renderer(msg) {
);
return {success: true};
case 'copyTexture':
if (!msg.data) return {error: "Missing texture data"};
var tex_id = msg.data.texture_id;
if (!tex_id || !resources.texture[tex_id]) return {error: "Invalid texture id"};
var tex = resources.texture[tex_id];
// Use the texture method with normalized coordinates
ren.texture(
tex,
msg.data.src || {x:0, y:0, width:tex.width, height:tex.height},
msg.data.dest || {x:0, y:0, width:tex.width, height:tex.height},
0, // No rotation
{x:0, y:0} // Top-left anchor
);
return {success: true};
case 'sprite':
if (!msg.data || !msg.data.sprite) return {error: "Missing sprite data"};
ren.sprite(msg.data.sprite);
@@ -404,13 +420,35 @@ function handle_renderer(msg) {
return {id: surf_id};
case 'loadTexture':
if (!msg.data || !msg.data.surface_id) return {error: "Missing surface_id"};
var surf = resources.surface[msg.data.surface_id];
if (!surf) return {error: "Invalid surface id"};
var tex = ren.load_texture(surf);
if (!msg.data) return {error: "Missing data"};
var tex;
// Load from surface ID
if (msg.data.surface_id) {
var surf = resources.surface[msg.data.surface_id];
if (!surf) return {error: "Invalid surface id"};
tex = ren.load_texture(surf);
}
// Load from raw surface object (for graphics module)
else if (msg.data.surface) {
tex = ren.load_texture(msg.data);
}
// Direct surface data
else if (msg.data.width && msg.data.height) {
tex = ren.load_texture(msg.data);
}
else {
return {error: "Must provide surface_id or surface data"};
}
if (!tex) return {error: "Failed to load texture"};
var tex_id = allocate_id();
resources.texture[tex_id] = tex;
return {id: tex_id};
return {
id: tex_id,
width: tex.width,
height: tex.height
};
case 'createTexture':
if (!msg.data || !msg.data.width || !msg.data.height) {

View File

@@ -1,6 +1,12 @@
(function engine() {
prosperon.DOC = Symbol('+documentation+') // Symbol for documentation references
globalThis.log = new Proxy({}, {
get(target,prop,receiver) {
return function() {}
}
})
var listeners = new Map()
prosperon.on = function(type, callback) {
@@ -672,7 +678,7 @@ function handle_message(msg) {
delete msg.data
letter[HEADER] = msg
if (msg.return) {
console.log(`Received a message for the return id ${msg.return}`)
log.trace(`Received a message for the return id ${msg.return}`)
var fn = replies[msg.return]
if (!fn) throw new Error(`Could not find return function for message ${msg.return}`)
fn(letter)

View File

@@ -1,8 +1,12 @@
var graphics = use('graphics')
var renderer_actor = arg[0]
var renderer_id = arg[1]
var graphics = use('graphics', renderer_actor, renderer_id)
var math = use('math')
var util = use('util')
var os = use('os')
var geometry = use('geometry')
var Color = use('color')
var draw = {}
draw[prosperon.DOC] = `
@@ -13,23 +17,10 @@ for lines, rectangles, text, sprite drawing, etc. Immediate mode.
// Draw command accumulator
var commands = []
// Renderer info set by moth
var renderer_actor = null
var renderer_id = null
// Prototype object for commands
var command_proto = null
// Set the renderer
draw.set_renderer = function(actor, id) {
renderer_actor = actor
renderer_id = id
// Create prototype object with common fields
command_proto = {
kind: "renderer",
id: id
}
var command_proto = {
kind: "renderer",
id: renderer_id
}
// Clear accumulated commands
@@ -484,12 +475,58 @@ draw.image = function image(image, rect = [0,0], rotation = 0, anchor = [0,0], s
if (!image) throw Error('Need an image to render.')
if (typeof image === "string")
image = graphics.texture(image)
rect.width ??= image.texture.width
rect.height ??= image.texture.height
info ??= image_info;
// TODO: Handle texture loading and sending texture_id
// For now, we skip image rendering as it requires texture management
// Ensure rect has proper structure
if (Array.isArray(rect)) {
rect = {x: rect[0], y: rect[1], width: image.width, height: image.height}
} else {
rect.width ??= image.width
rect.height ??= image.height
}
info = Object.assign({}, image_info, info);
// Get the GPU texture (might be loading)
var texture = image.gpu;
if (!texture) {
// Texture not loaded yet, skip drawing
return;
}
// Set texture filtering mode
if (info.mode) {
add_command("set", null, "textureFilter", info.mode === 'linear' ? 'linear' : 'nearest')
}
// Set color if specified
if (info.color) {
add_command("set", null, "drawColor", info.color)
}
// Calculate source rectangle from image.rect (UV coords)
var src_rect = {
x: image.rect.x * texture.width,
y: image.rect.y * texture.height,
width: image.rect.width * texture.width,
height: image.rect.height * texture.height
}
// Handle flipping
if (info.flip_x) {
src_rect.x += src_rect.width;
src_rect.width = -src_rect.width;
}
if (info.flip_y) {
src_rect.y += src_rect.height;
src_rect.height = -src_rect.height;
}
// Draw the texture
add_command("copyTexture", {
texture_id: texture.id,
src: src_rect,
dest: rect
})
}
function software_circle(pos, radius)

View File

@@ -6,14 +6,18 @@ Includes both JavaScript and C-implemented routines for creating geometry buffer
rectangle packing, etc.
`
var renderer_actor = arg[0]
var renderer_id = arg[1]
var io = use('io')
var os = use('os')
var res = use('resources')
var render = use('render')
var json = use('json')
var GPU = Symbol()
var CPU = Symbol()
var LASTUSE = Symbol()
var LOADING = Symbol()
var cache = new Map()
@@ -21,9 +25,26 @@ var cache = new Map()
graphics.Image = {
get gpu() {
this[LASTUSE] = os.now();
if (!this[GPU]) {
this[GPU] = render.load_texture(this[CPU]);
decorate_rect_px(this);
if (!this[GPU] && !this[LOADING]) {
this[LOADING] = true;
var self = this;
// Send message to load texture
send(renderer_actor, {
kind: "renderer",
id: renderer_id,
op: "loadTexture",
data: this[CPU]
}, function(response) {
if (response.error) {
console.error("Failed to load texture:", response.error);
self[LOADING] = false;
} else {
self[GPU] = response;
decorate_rect_px(self);
self[LOADING] = false;
}
});
}
return this[GPU]
@@ -33,18 +54,23 @@ graphics.Image = {
get cpu() {
this[LASTUSE] = os.now();
if (!this[CPU]) this[CPU] = render.read_texture(this[GPU])
// Note: Reading texture back from GPU requires async operation
// For now, return the CPU data if available
return this[CPU]
},
get surface() { return this.cpu },
get width() {
return this[GPU].width
if (this[GPU]) return this[GPU].width
if (this[CPU]) return this[CPU].width
return 0
},
get height() {
return this[GPU].height
if (this[GPU]) return this[GPU].height
if (this[CPU]) return this[CPU].height
return 0
},
unload_gpu() {
@@ -79,15 +105,11 @@ function decorate_rect_px(img) {
function make_handle(obj)
{
var image = Object.create(graphics.Image);
if (obj.surface) {
im
}
return Object.assign(Object.create(graphics.Image), {
rect:{x:0,y:0,width:1,height:1},
[CPU]:obj,
[GPU]:undefined,
[LOADING]:false,
[LASTUSE]:os.now()
})
}
@@ -188,19 +210,18 @@ graphics.texture_from_data = function(data)
{
if (!(data instanceof ArrayBuffer)) return undefined
var img = {
surface: graphics.make_texture(data)
}
render.load_texture(img)
decorate_rect_px(img)
var surface = graphics.make_texture(data);
var img = make_handle(surface);
return img
// Trigger GPU load (async)
img.gpu;
return img;
}
graphics.from_surface = function(id, surf)
{
return make_handle(surf)
var img = { surface: surf }
}
graphics.from = function(id, data)
@@ -307,10 +328,22 @@ graphics.get_font = function get_font(path, size) {
var data = io.slurpbytes(fullpath)
var font = graphics.make_font(data,size)
font.texture = render.load_texture(font.surface)
console.log('loaded font texture')
console.log(json.encode(font.texture))
// Load font texture via renderer actor (async)
send(renderer_actor, {
kind: "renderer",
id: renderer_id,
op: "loadTexture",
data: font.surface
}, function(response) {
if (response.error) {
console.error("Failed to load font texture:", response.error);
} else {
font.texture = response;
console.log('loaded font texture');
console.log(json.encode(font.texture));
}
});
fontcache[fontstr] = font
@@ -341,23 +374,6 @@ Builds a single geometry mesh for all sprite-type commands in the queue, storing
so they can be rendered in one draw call.
`
graphics.make_sprite_mesh[prosperon.DOC] = `
:param sprites: An array of sprite objects, each containing .rect (or transform), .src (UV region), .color, etc.
:param oldMesh (optional): An existing mesh object to reuse/resize if possible.
:return: A GPU mesh object with pos, uv, color, and indices buffers for all sprites.
Given an array of sprites, build a single geometry mesh for rendering them.
`
graphics.make_sprite_queue[prosperon.DOC] = `
:param sprites: An array of sprite objects.
:param camera: (unused in the C code example) Typically a camera or transform for sorting?
:param pipeline: A pipeline object for rendering.
:param sort: An integer or boolean for whether to sort sprites; if truthy, sorts by layer & texture.
:return: An array of pipeline commands: geometry with mesh references, grouped by image.
Given an array of sprites, optionally sort them, then build a queue of pipeline commands.
Each group with a shared image becomes one command.
`
graphics.make_text_buffer[prosperon.DOC] = `
:param text: The string to render.
:param rect: A rectangle specifying position and possibly wrapping.

View File

@@ -1 +0,0 @@
return use('sdl_render')

View File

@@ -1,125 +0,0 @@
var render = {}
var context
var util = use('util')
render.initialize = function(config)
{
var default_conf = {
title:`Prosperon [${prosperon.version}-${prosperon.revision}]`,
width: 1280,
height: 720,
// icon: graphics.make_texture(io.slurpbytes('icons/moon.gif')),
high_dpi:0,
alpha:1,
fullscreen:0,
sample_count:1,
enable_clipboard:true,
enable_dragndrop: true,
max_dropped_files: 1,
swap_interval: 1,
name: "Prosperon",
version:prosperon.version + "-" + prosperon.revision,
identifier: "world.pockle.prosperon",
creator: "Pockle World LLC",
copyright: "Copyright Pockle World 2025",
type: "game",
url: "https://prosperon.dev"
}
config.__proto__ = default_conf
prosperon.window = prosperon.engine_start(config)
context = prosperon.window.make_renderer()
context.logical_size([config.resolution_x, config.resolution_y], config.mode)
}
render.sprite = function(sprite)
{
context.sprite(sprite)
}
// img here is the engine surface
render.load_texture = function(surface)
{
return context.load_texture(surface)
}
var current_color = Color.white
render.image = function(image, rect, rotation, anchor, shear, info)
{
// rect.width = image.rect_px.width;
// rect.height = image.rect_px.height;
image.texture.mode(info.mode)
context.texture(image.texture, image.rect_px, rect, rotation, anchor);
}
render.clip = function(rect)
{
context.clip(rect)
}
render.line = function(points)
{
context.line(points)
}
render.point = function(pos)
{
context.point(pos)
}
render.rectangle = function(rect)
{
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(image, mesh, pipeline)
{
context.geometry(image, mesh)
}
render.slice9 = function(image, rect, slice, info, pipeline)
{
context.slice9(image.texture, image.rect_px, util.normalizeSpacing(slice), rect);
}
render.get_image = function(rect)
{
return context.get_image(rect)
}
render.clear = function(color)
{
if (color) context.draw_color(color)
context.clear()
}
render.present = function()
{
context.present()
}
render.camera = function(cam)
{
context.camera(cam);
}
return render

View File

@@ -592,6 +592,7 @@ void script_startup(prosperon_rt *prt, void (*hook)(JSContext*))
JS_AddIntrinsicMapSet(js);
JS_AddIntrinsicTypedArrays(js);
JS_AddIntrinsicWeakRef(js);
JS_AddIntrinsicProxy(js);
JS_SetContextOpaque(js, prt);
prt->context = js;

238
tests/draw2d.js Normal file
View File

@@ -0,0 +1,238 @@
// Test draw2d module without moth framework
var draw2d
var graphics
var os = use('os');
use('tracy').level = 1
// Create SDL video actor
var video = use('sdl_video');
var video_actor = {__ACTORDATA__:{id:video}};
var window_id = null;
var renderer_id = null;
// Create window
send(video_actor, {
kind: "window",
op: "create",
data: {
title: "Draw2D Test",
width: 800,
height: 600
}
}, function(response) {
if (response.error) {
console.error("Failed to create window:", response.error);
return;
}
window_id = response.id;
console.log("Created window with id:", window_id);
// Create renderer
send(video_actor, {
kind: "window",
op: "makeRenderer",
id: window_id
}, function(response) {
if (response.error) {
console.error("Failed to create renderer:", response.error);
return;
}
renderer_id = response.id;
console.log("Created renderer with id:", renderer_id);
// Configure draw2d and graphics
draw2d = use('draw2d', video_actor, renderer_id)
graphics = use('graphics', video_actor, renderer_id)
// Start drawing after a short delay
$_.delay(start_drawing, 0.1);
});
});
function start_drawing() {
var frame = 0;
var start_time = os.now();
// Load an image
var bunny_image = null;
try {
bunny_image = graphics.texture('tests/bunny.png');
console.log("Loaded bunny image");
} catch (e) {
console.error("Failed to load bunny image:", e);
}
function draw_frame() {
frame++;
var t = os.now() - start_time;
// Clear the screen with a dark background
send(video_actor, {
kind: "renderer",
id: renderer_id,
op: "set",
prop: "drawColor",
value: [0.1, 0.1, 0.15, 1]
});
send(video_actor, {
kind: "renderer",
id: renderer_id,
op: "clear"
});
// Clear draw2d commands
draw2d.clear();
// Draw some rectangles
draw2d.rectangle(
{x: 50, y: 50, width: 100, height: 100},
{thickness: 0, color: [1, 0, 0, 1]}
);
draw2d.rectangle(
{x: 200, y: 50, width: 100, height: 100},
{thickness: 5, color: [0, 1, 0, 1]}
);
draw2d.rectangle(
{x: 350, y: 50, width: 100, height: 100},
{thickness: 2, color: [0, 0, 1, 1], radius: 20}
);
// Draw circles with animation
var radius = 30 + Math.sin(t * 2) * 10;
draw2d.circle(
[100, 250],
radius,
{color: [1, 1, 0, 1], thickness: 0}
);
draw2d.circle(
[250, 250],
40,
{color: [1, 0, 1, 1], thickness: 3}
);
// Draw ellipse
draw2d.ellipse(
[400, 250],
[60, 30],
{color: [0, 1, 1, 1], thickness: 2}
);
// Draw lines
var line_y = 350 + Math.sin(t * 3) * 20;
draw2d.line(
[[50, line_y], [150, line_y + 50], [250, line_y]],
{color: [1, 0.5, 0, 1], thickness: 2}
);
// Draw cross
draw2d.cross(
[350, 375],
25,
{color: [0.5, 1, 0.5, 1], thickness: 3}
);
// Draw arrow
draw2d.arrow(
[450, 350],
[550, 400],
15,
30,
{color: [1, 1, 1, 1], thickness: 2}
);
// Draw partial circle (arc)
draw2d.circle(
[150, 480],
50,
{
color: [0.8, 0.8, 1, 1],
thickness: 5,
start: 0.25,
end: 0.75
}
);
// Draw filled partial ellipse
draw2d.ellipse(
[350, 480],
[80, 40],
{
color: [1, 0.8, 0.8, 1],
thickness: 0,
start: 0,
end: 0.6
}
);
// Draw some points in a pattern
var point_count = 20;
for (var i = 0; i < point_count; i++) {
var angle = (i / point_count) * Math.PI * 2;
var r = 30 + Math.sin(t * 4 + i * 0.5) * 10;
var px = 650 + Math.cos(angle) * r;
var py = 300 + Math.sin(angle) * r;
draw2d.point(
[px, py],
3,
{color: [1, 0.5 + Math.sin(t * 2 + i) * 0.5, 0.5, 1]}
);
}
// Draw the bunny image if loaded
if (bunny_image) {
// Static bunny
draw2d.image(bunny_image, {x: 500, y: 450, width: 64, height: 64});
// Rotating bunny
var rotation = t * 0.5;
draw2d.image(
bunny_image,
{x: 600, y: 450, width: 64, height: 64},
rotation,
[0.5, 0.5] // Center anchor
);
// Bouncing bunny with tint
var bounce_y = 500 + Math.sin(t * 3) * 20;
draw2d.image(
bunny_image,
{x: 700, y: bounce_y, width: 48, height: 48},
0,
[0.5, 1], // Bottom center anchor
[0, 0], // No shear
{color: [1, 0.5, 0.5, 1]} // Red tint
);
}
// Flush all commands to renderer
draw2d.flush();
// Present the frame
send(video_actor, {
kind: "renderer",
id: renderer_id,
op: "present"
});
// Schedule next frame (60 FPS)
if (frame < 600) { // Run for 10 seconds
$_.delay(draw_frame, 1/60);
} else {
console.log("Test completed - drew", frame, "frames");
$_.delay($_.stop, 0.5);
}
}
draw_frame();
}
// Stop after 12 seconds if not already stopped
$_.delay($_.stop, 12);