prosperon module
This commit is contained in:
@@ -15,3 +15,5 @@ main = true
|
||||
main = true
|
||||
[actors.prosperon]
|
||||
main = true
|
||||
[actors.accio]
|
||||
main=true
|
||||
426
prosperon/prosperon.cm
Normal file
426
prosperon/prosperon.cm
Normal file
@@ -0,0 +1,426 @@
|
||||
var prosperon = {}
|
||||
|
||||
var os = use('os');
|
||||
var io = use('io');
|
||||
var rasterize = use('rasterize');
|
||||
var time = use('time')
|
||||
var tilemap = use('tilemap')
|
||||
var geometry = use('geometry')
|
||||
var res = use('resources')
|
||||
|
||||
|
||||
var video = arg[0]
|
||||
var graphics = use('graphics', arg[0])
|
||||
|
||||
var camera = {}
|
||||
|
||||
function updateCameraMatrix(cam) {
|
||||
def win_w = logical.width
|
||||
def win_h = logical.height
|
||||
def view_w = (cam.size?.[0] ?? win_w) / cam.zoom
|
||||
def view_h = (cam.size?.[1] ?? win_h) / cam.zoom
|
||||
|
||||
def ox = cam.pos[0] - view_w * (cam.anchor?.[0] ?? 0)
|
||||
def oy = cam.pos[1] - view_h * (cam.anchor?.[1] ?? 0)
|
||||
|
||||
def vx = (cam.viewport?.x ?? 0) * win_w
|
||||
def vy = (cam.viewport?.y ?? 0) * win_h
|
||||
def vw = (cam.viewport?.width ?? 1) * win_w
|
||||
def vh = (cam.viewport?.height ?? 1) * win_h
|
||||
|
||||
def sx = vw / view_w
|
||||
def sy = vh / view_h // flip-Y later
|
||||
|
||||
/* affine matrix that SDL wants (Y going down) */
|
||||
cam.a = sx
|
||||
cam.c = vx - sx * ox
|
||||
cam.e = -sy // <-- minus = flip Y
|
||||
cam.f = vy + vh + sy * oy
|
||||
|
||||
/* convenience inverses */
|
||||
cam.ia = 1 / cam.a
|
||||
cam.ic = -cam.c / cam.a
|
||||
cam.ie = 1 / cam.e
|
||||
cam.if = -cam.f / cam.e
|
||||
|
||||
camera = cam
|
||||
}
|
||||
|
||||
//---- forward transform ----
|
||||
function worldToScreenPoint([x,y], camera) {
|
||||
return {
|
||||
x: camera.a * x + camera.c,
|
||||
y: camera.e * y + camera.f
|
||||
};
|
||||
}
|
||||
|
||||
//---- inverse transform ----
|
||||
function screenToWorldPoint(pos, camera) {
|
||||
return {
|
||||
x: camera.ia * pos[0] + camera.ic,
|
||||
y: camera.ie * pos[1] + camera.if
|
||||
};
|
||||
}
|
||||
|
||||
//---- rectangle (two corner) ----
|
||||
function worldToScreenRect({x,y,width,height}, camera) {
|
||||
// map bottom-left and top-right
|
||||
def x1 = camera.a * x + camera.c;
|
||||
def y1 = camera.e * y + camera.f;
|
||||
def x2 = camera.a * (x + width) + camera.c;
|
||||
def y2 = camera.e * (y + height) + camera.f;
|
||||
|
||||
return {
|
||||
x:Math.min(x1,x2),
|
||||
y:Math.min(y1,y2),
|
||||
width:Math.abs(x2-x1),
|
||||
height:Math.abs(y2-y1)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
var gameactor
|
||||
|
||||
var images = {}
|
||||
|
||||
var renderer_commands = []
|
||||
|
||||
var win_size = {width:500,height:500}
|
||||
var logical = {width:500,height:500}
|
||||
|
||||
// Convert high-level draw commands to low-level renderer commands
|
||||
function translate_draw_commands(commands) {
|
||||
if (!graphics) return
|
||||
|
||||
renderer_commands.length = 0
|
||||
|
||||
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 "camera":
|
||||
updateCameraMatrix(cmd.camera, win_size.width, win_size.height)
|
||||
break
|
||||
|
||||
case "draw_rect":
|
||||
cmd.rect = worldToScreenRect(cmd.rect, camera)
|
||||
// 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') {
|
||||
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') {
|
||||
renderer_commands.push({
|
||||
op: "rects",
|
||||
data: {rects: raster_result.data}
|
||||
})
|
||||
}
|
||||
} else {
|
||||
renderer_commands.push({
|
||||
op: "fillRect",
|
||||
data: {rect: cmd.rect}
|
||||
})
|
||||
}
|
||||
break
|
||||
|
||||
case "draw_circle":
|
||||
case "draw_ellipse":
|
||||
cmd.pos = worldToScreenPoint(cmd.pos, camera)
|
||||
// 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.map(p => {
|
||||
var pt = worldToScreenPoint(p, camera)
|
||||
return [pt.x, pt.y]
|
||||
})}
|
||||
})
|
||||
break
|
||||
|
||||
case "draw_point":
|
||||
cmd.pos = worldToScreenPoint(cmd.pos, camera)
|
||||
renderer_commands.push({
|
||||
op: "point",
|
||||
data: {points: [cmd.pos]}
|
||||
})
|
||||
break
|
||||
|
||||
case "draw_image":
|
||||
var img = graphics.texture(cmd.image)
|
||||
var gpu = img.gpu
|
||||
if (!gpu) break
|
||||
|
||||
cmd.rect.width ??= img.width
|
||||
cmd.rect.height ??= img.height
|
||||
cmd.rect = worldToScreenRect(cmd.rect, camera)
|
||||
|
||||
renderer_commands.push({
|
||||
op: "texture",
|
||||
data: {
|
||||
texture_id: gpu.id,
|
||||
dst: cmd.rect,
|
||||
src: img.rect
|
||||
}
|
||||
})
|
||||
|
||||
break
|
||||
|
||||
case "draw_text":
|
||||
if (!cmd.text) break
|
||||
if (!cmd.pos) break
|
||||
var rect = worldToScreenRect({x:cmd.pos.x, y:cmd.pos.y, width:8, height:8}, camera)
|
||||
var pos = {x: rect.x, y: rect.y}
|
||||
renderer_commands.push({
|
||||
op: "debugText",
|
||||
data: {
|
||||
pos,
|
||||
text: cmd.text
|
||||
}
|
||||
})
|
||||
break
|
||||
|
||||
case "draw_slice9":
|
||||
var img = graphics.texture(cmd.image)
|
||||
var gpu = img.gpu
|
||||
if (!gpu) break
|
||||
|
||||
cmd.rect = worldToScreenRect(cmd.rect, camera)
|
||||
|
||||
renderer_commands.push({
|
||||
op: "texture9Grid",
|
||||
data: {
|
||||
texture_id: gpu.id,
|
||||
src: img.rect,
|
||||
leftWidth: cmd.slice,
|
||||
rightWidth: cmd.slice,
|
||||
topHeight: cmd.slice,
|
||||
bottomHeight: cmd.slice,
|
||||
scale: 1.0,
|
||||
dst: cmd.rect
|
||||
}
|
||||
})
|
||||
break
|
||||
|
||||
case "tilemap":
|
||||
// Group tiles by texture to batch draw calls
|
||||
var textureGroups = {}
|
||||
var tilePositions = []
|
||||
|
||||
// Collect all tiles and their positions
|
||||
tilemap.for(cmd.tilemap, (tile, {x,y}) => {
|
||||
if (tile) {
|
||||
tilePositions.push({tile, x, y})
|
||||
}
|
||||
})
|
||||
|
||||
// Group tiles by texture
|
||||
tilePositions.forEach(({tile, x, y}) => {
|
||||
var img = graphics.texture(tile)
|
||||
if (img && img.gpu) {
|
||||
var texId = img.gpu.id
|
||||
if (!textureGroups[texId]) {
|
||||
textureGroups[texId] = {
|
||||
texture: img,
|
||||
tiles: []
|
||||
}
|
||||
}
|
||||
textureGroups[texId].tiles.push({x, y, img})
|
||||
}
|
||||
})
|
||||
|
||||
// Generate draw commands for each texture group
|
||||
Object.keys(textureGroups).forEach(texId => {
|
||||
var group = textureGroups[texId]
|
||||
var tiles = group.tiles
|
||||
|
||||
// Create a temporary tilemap with only tiles from this texture
|
||||
// Apply tilemap position to the offset to shift the world coordinates
|
||||
var tempMap = {
|
||||
tiles: [],
|
||||
offset_x: cmd.tilemap.offset_x + (cmd.tilemap.pos.x / cmd.tilemap.size_x),
|
||||
offset_y: cmd.tilemap.offset_y + (cmd.tilemap.pos.y / cmd.tilemap.size_y),
|
||||
size_x: cmd.tilemap.size_x,
|
||||
size_y: cmd.tilemap.size_y
|
||||
}
|
||||
|
||||
// Build sparse array for this texture's tiles
|
||||
tiles.forEach(({x, y, img}) => {
|
||||
var arrayX = x - cmd.tilemap.offset_x
|
||||
var arrayY = y - cmd.tilemap.offset_y
|
||||
if (!tempMap.tiles[arrayX]) tempMap.tiles[arrayX] = []
|
||||
tempMap.tiles[arrayX][arrayY] = img
|
||||
})
|
||||
// Generate geometry for this texture group
|
||||
var geom = geometry.tilemap_to_data(cmd.tilemap)
|
||||
geom.texture_id = parseInt(texId)
|
||||
|
||||
renderer_commands.push({
|
||||
op: "geometry_raw",
|
||||
data: geom
|
||||
})
|
||||
})
|
||||
break
|
||||
}
|
||||
})
|
||||
|
||||
return renderer_commands
|
||||
}
|
||||
|
||||
///// input /////
|
||||
var input = use('input')
|
||||
var input_cb
|
||||
var input_rate = 1/60
|
||||
function poll_input() {
|
||||
send(video, {kind:'input', op:'get'}, evs => {
|
||||
for (var ev of evs) {
|
||||
if (ev.type == 'window_pixel_size_changed') {
|
||||
win_size.width = ev.width
|
||||
win_size.height = ev.height
|
||||
}
|
||||
|
||||
if (ev.type == 'quit')
|
||||
$_.stop()
|
||||
|
||||
if (ev.type.includes('key')) {
|
||||
if (ev.key)
|
||||
ev.key = input.keyname(ev.key)
|
||||
}
|
||||
|
||||
if (ev.type.startsWith('mouse_'))
|
||||
ev.pos.y = -ev.pos.y + logical.height
|
||||
}
|
||||
|
||||
input_cb(evs)
|
||||
})
|
||||
$_.delay(poll_input, input_rate)
|
||||
}
|
||||
|
||||
prosperon.input = function(fn)
|
||||
{
|
||||
input_cb = fn
|
||||
poll_input()
|
||||
}
|
||||
|
||||
// 2) helper to build & send a batch, then call done()
|
||||
prosperon.create_batch = function create_batch(draw_cmds, done) {
|
||||
def batch = [
|
||||
{op:'set', prop:'drawColor', value:{r:0.1,g:0.1,b:0.15,a:1}},
|
||||
{op:'clear'}
|
||||
]
|
||||
if (draw_cmds && draw_cmds.length)
|
||||
batch.push(...translate_draw_commands(draw_cmds))
|
||||
|
||||
batch.push(
|
||||
{op:'set', prop:'drawColor', value:{r:1,g:1,b:1,a:1}},
|
||||
{op:'imgui_render'},
|
||||
{op:'present'}
|
||||
)
|
||||
|
||||
send(video, {kind:'renderer', op:'batch', data:batch}, done)
|
||||
}
|
||||
|
||||
////////// dmon hot reload ////////
|
||||
function poll_file_changes() {
|
||||
dmon.poll(e => {
|
||||
if (e.action == 'modify' || e.action == 'create') {
|
||||
// Check if it's an image file
|
||||
var ext = e.file.split('.').pop().toLowerCase()
|
||||
var imageExts = ['png', 'jpg', 'jpeg', 'gif', 'bmp', 'tga', 'webp', 'qoi', 'ase', 'aseprite']
|
||||
|
||||
if (imageExts.includes(ext)) {
|
||||
// Try to find the full path for this image
|
||||
var possiblePaths = [
|
||||
e.file,
|
||||
e.root + e.file,
|
||||
res.find_image(e.file.split('/').pop().split('.')[0])
|
||||
].filter(p => p)
|
||||
|
||||
for (var path of possiblePaths) {
|
||||
graphics.tex_hotreload(path)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Schedule next poll in 0.5 seconds
|
||||
$_.delay(poll_file_changes, 0.5)
|
||||
}
|
||||
|
||||
var dmon = use('dmon')
|
||||
prosperon.dmon = function()
|
||||
{
|
||||
dmon.watch('.')
|
||||
poll_file_changes()
|
||||
}
|
||||
|
||||
var window_cmds = {
|
||||
size(size) {
|
||||
send(video, {kind: 'window', op:'set', data: {property: 'size', value: size}})
|
||||
},
|
||||
}
|
||||
|
||||
prosperon.set_window = function(config)
|
||||
{
|
||||
for (var c in config)
|
||||
if (window_cmds[c]) window_cmds[c](config[c])
|
||||
}
|
||||
|
||||
var renderer_cmds = {
|
||||
resolution(size) {
|
||||
send(video, {kind:"renderer", op:'set', prop:'logicalPresentation', value: {...e}})
|
||||
}
|
||||
}
|
||||
|
||||
prosperon.set_renderer = function(config)
|
||||
{
|
||||
for (var c in config)
|
||||
if (renderer_cmds[c]) renderer_cmds[c](config[c])
|
||||
}
|
||||
|
||||
return prosperon
|
||||
@@ -10,7 +10,7 @@ function tilemap()
|
||||
return this;
|
||||
}
|
||||
|
||||
tilemap.for = function (map, fn) {
|
||||
tilemap.for = function tilemap_for(map, fn) {
|
||||
for (var x = 0; x < map.tiles.length; x++) {
|
||||
if (!map.tiles[x]) continue;
|
||||
for (var y = 0; y < map.tiles[x].length; y++) {
|
||||
|
||||
@@ -20,6 +20,11 @@ var LOADING = Symbol()
|
||||
|
||||
var cache = {}
|
||||
|
||||
graphics.setup = function(renderer)
|
||||
{
|
||||
renderer_actor = renderer
|
||||
}
|
||||
|
||||
// Image constructor function
|
||||
graphics.Image = function(surfaceData) {
|
||||
// Initialize private properties
|
||||
@@ -402,12 +407,6 @@ graphics.get_font = function get_font(path, size) {
|
||||
|
||||
return font
|
||||
}
|
||||
graphics.get_font[cell.DOC] = `
|
||||
:param path: A string path to a font file, optionally with ".size" appended.
|
||||
:param size: Pixel size of the font, if not included in 'path'.
|
||||
:return: A font object with .surface and .texture for rendering text.
|
||||
Load a font from file if not cached, or retrieve from cache if already loaded.
|
||||
`
|
||||
|
||||
graphics.queue_sprite_mesh = function(queue) {
|
||||
var sprites = queue.filter(x => x.type == 'sprite')
|
||||
@@ -420,72 +419,5 @@ graphics.queue_sprite_mesh = function(queue) {
|
||||
}
|
||||
return [mesh.pos, mesh.uv, mesh.color, mesh.indices]
|
||||
}
|
||||
graphics.queue_sprite_mesh[cell.DOC] = `
|
||||
:param queue: An array of draw commands, some of which are {type:'sprite'} objects.
|
||||
:return: An array of references to GPU buffers [pos,uv,color,indices].
|
||||
Builds a single geometry mesh for all sprite-type commands in the queue, storing first_index/num_indices
|
||||
so they can be rendered in one draw call.
|
||||
`
|
||||
|
||||
graphics.make_text_buffer[cell.DOC] = `
|
||||
:param text: The string to render.
|
||||
:param rect: A rectangle specifying position and possibly wrapping.
|
||||
:param angle: Rotation angle (unused or optional).
|
||||
:param color: A color for the text (could be a vec4).
|
||||
:param wrap: The width in pixels to wrap text, or 0 for no wrap.
|
||||
:param font: A font object created by graphics.make_font or graphics.get_font.
|
||||
:return: A geometry buffer mesh (pos, uv, color, indices) for rendering text.
|
||||
Generate a GPU buffer mesh of text quads for rendering with a font, etc.
|
||||
`
|
||||
|
||||
graphics.rectpack[cell.DOC] = `
|
||||
:param width: The width of the area to pack into.
|
||||
:param height: The height of the area to pack into.
|
||||
:param sizes: An array of [w,h] pairs for the rectangles to pack.
|
||||
:return: An array of [x,y] coordinates placing each rect, or null if they don't fit.
|
||||
Perform a rectangle packing using the stbrp library. Return positions for each rect.
|
||||
`
|
||||
|
||||
graphics.make_texture[cell.DOC] = `
|
||||
:param data: Raw image bytes (PNG, JPG, etc.) as an ArrayBuffer.
|
||||
:return: An SDL_Surface object representing the decoded image in RAM, for use with GPU or software rendering.
|
||||
Convert raw image bytes into an SDL_Surface object.
|
||||
`
|
||||
|
||||
graphics.make_gif[cell.DOC] = `
|
||||
:param data: An ArrayBuffer containing GIF data.
|
||||
:return: An object with frames[], each frame having its own .surface. Some also have a .texture for GPU use.
|
||||
Load a GIF, returning its frames. If it's a single-frame GIF, the result may have .surface only.
|
||||
`
|
||||
|
||||
graphics.make_aseprite[cell.DOC] = `
|
||||
:param data: An ArrayBuffer containing Aseprite (ASE) file data.
|
||||
:return: An object containing frames or animations, each with .surface. May also have top-level .surface for a single-layer case.
|
||||
Load an Aseprite/ASE file from an array of bytes, returning frames or animations.
|
||||
`
|
||||
|
||||
graphics.cull_sprites[cell.DOC] = `
|
||||
:param sprites: An array of sprite objects (each has rect or transform).
|
||||
:param camera: A camera or bounding rectangle defining the view area.
|
||||
:return: A new array of sprites that are visible in the camera's view.
|
||||
Filter an array of sprites to only those visible in the provided camera’s view.
|
||||
`
|
||||
|
||||
graphics.make_font[cell.DOC] = `
|
||||
:param data: TTF/OTF file data as an ArrayBuffer.
|
||||
:param size: Pixel size for rendering glyphs.
|
||||
:return: A font object with surface, texture, and glyph data, for text rendering with make_text_buffer.
|
||||
Load a font from TTF/OTF data at the given size.
|
||||
`
|
||||
|
||||
graphics.make_line_prim[cell.DOC] = `
|
||||
:param points: An array of [x,y] points forming the line.
|
||||
:param thickness: The thickness (width) of the polyline.
|
||||
:param startCap: (Unused) Possibly the type of cap for the start.
|
||||
:param endCap: (Unused) Possibly the type of cap for the end.
|
||||
:param color: A color to apply to the line.
|
||||
:return: A geometry mesh object suitable for rendering the line via a pipeline command.
|
||||
Build a GPU mesh representing a thick polyline from an array of points, using parsl or a similar library under the hood.
|
||||
`
|
||||
|
||||
return graphics
|
||||
Reference in New Issue
Block a user