Files
cell/prosperon/prosperon.cm
2025-07-27 17:08:29 -05:00

506 lines
14 KiB
Plaintext

var prosperon = {}
// This file is hard coded for the SDL renderer case
var video = use('sdl_video')
var imgui = use('imgui')
var surface = use('surface')
var default_window = {
// Basic properties
title: "Prosperon Window",
width: 640,
height: 480,
// Position - can be numbers or "centered"
x: null, // SDL_WINDOWPOS_null by default
y: null, // SDL_WINDOWPOS_null by default
// Window behavior flags
resizable: true,
fullscreen: false,
hidden: false,
borderless: false,
alwaysOnTop: false,
minimized: false,
maximized: false,
// Input grabbing
mouseGrabbed: false,
keyboardGrabbed: false,
// Display properties
highPixelDensity: false,
transparent: false,
opacity: 1.0, // 0.0 to 1.0
// Focus behavior
notFocusable: false,
// Special window types (mutually exclusive)
utility: false, // Utility window (not in taskbar)
tooltip: false, // Tooltip window (requires parent)
popupMenu: false, // Popup menu window (requires parent)
// Graphics API flags (let SDL choose if not specified)
opengl: false, // Force OpenGL context
vulkan: false, // Force Vulkan context
metal: false, // Force Metal context (macOS)
// Advanced properties
parent: null, // Parent window for tooltips/popups/modal
modal: false, // Modal to parent window (requires parent)
externalGraphicsContext: false, // Use external graphics context
// Input handling
textInput: true, // Enable text input on creation
}
var win_config = arg[0] || {}
win_config.__proto__ = default_window
var window = new video.window(win_config)
var renderer = window.make_renderer()
// Initialize ImGui with the window and renderer
imgui.init(window, renderer)
imgui.newframe()
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 input = use('input')
var graphics = use('graphics')
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) {
renderer_commands.length = 0
commands.forEach(function(cmd) {
if (cmd.material && cmd.material.color && typeof cmd.material.color == 'object') {
renderer.drawColor = 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)
renderer.fillRect(cmd.rect)
break
case "draw_line":
var points = cmd.points.map(p => {
var pt = worldToScreenPoint(p, camera)
return[pt.x, pt.y]
})
renderer.line(points)
break
case "draw_point":
cmd.pos = worldToScreenPoint(cmd.pos, camera)
renderer.point(cmd.pos)
break
case "draw_image":
var img = graphics.texture(cmd.image)
if (!img.gpu) {
var surf = new surface(img.cpu)
img.gpu = renderer.load_texture(surf)
}
var gpu = img.gpu
if (!cmd.scale) cmd.scale = {x:1,y:1}
cmd.rect.width ??= img.width
cmd.rect.height ??= img.height
cmd.rect.width = cmd.rect.width * cmd.scale.x
cmd.rect.height = cmd.rect.height * cmd.scale.y
cmd.rect = worldToScreenRect(cmd.rect, camera)
renderer.texture(
gpu,
img.rect,
cmd.rect,
0,
{x:0,y:0}
)
break
case "draw_text":
if (!cmd.text) break
if (!cmd.pos) break
// Get font from the font string (e.g., "smalle.16")
var font = graphics.get_font(cmd.font)
if (!font.gpu) {
var surf = new surface(font.surface)
font.gpu = renderer.load_texture(surf)
}
var gpu = font.gpu
// Create text geometry buffer
var text_mesh = graphics.make_text_buffer(
cmd.text,
{x: cmd.pos.x, y: cmd.pos.y},
[cmd.material.color.r, cmd.material.color.g, cmd.material.color.b, cmd.material.color.a],
cmd.wrap || 0,
font
)
if (!text_mesh) break
if (text_mesh.xy.length == 0) break
// Transform XY coordinates using camera matrix
var camera_params = [camera.a, camera.c, camera.e, camera.f]
var transformed_xy = geometry.transform_xy_blob(text_mesh.xy, camera_params)
// Create transformed geometry object
var geom = {
xy: transformed_xy,
xy_stride: text_mesh.xy_stride,
uv: text_mesh.uv,
uv_stride: text_mesh.uv_stride,
color: text_mesh.color,
color_stride: text_mesh.color_stride,
indices: text_mesh.indices,
num_vertices: text_mesh.num_vertices,
num_indices: text_mesh.num_indices,
size_indices: text_mesh.size_indices
}
renderer.geometry_raw(gpu, geom.xy, geom.xy_stride, geom.color, geom.color_stride, geom.uv, geom.uv_stride, geom.num_vertices, geom.indices, geom.num_indices, geom.size_indices)
break
case "tilemap":
// Get cached geometry commands from tilemap
var geometryCommands = cmd.tilemap.draw()
// Process each geometry command (one per texture)
for (var geomCmd of geometryCommands) {
var img = graphics.texture(geomCmd.image)
if (!img) continue
// Get GPU texture following draw_image pattern
if (!img.gpu) {
var surf = new surface(img.cpu)
img.gpu = renderer.load_texture(surf)
}
var gpu = img.gpu
// Transform geometry through camera and send to renderer
var geom = geomCmd.geometry
// Transform XY coordinates using camera matrix
var camera_params = [camera.a, camera.c, camera.e, camera.f]
var transformed_xy = geometry.transform_xy_blob(geom.xy, camera_params)
// Render directly instead of pushing to commands
renderer.geometry_raw(gpu, transformed_xy, geom.xy_stride, geom.color, geom.color_stride, geom.uv, geom.uv_stride, geom.num_vertices, geom.indices, geom.num_indices, geom.size_indices)
}
break
case "geometry":
var gpu
if (cmd.texture_id) {
// If texture_id provided, assume it's already a GPU texture
gpu = cmd.texture_id
} else {
// Fall back to looking up by image path
var img = graphics.texture(cmd.image)
if (!img) break
// Get GPU texture following draw_image pattern
if (!img.gpu) {
var surf = new surface(img.cpu)
img.gpu = renderer.load_texture(surf)
}
gpu = img.gpu
}
// Transform geometry through camera and send to renderer
var geom = cmd.geometry
// Transform XY coordinates using camera matrix
var camera_params = [camera.a, camera.c, camera.e, camera.f]
var transformed_xy = geometry.transform_xy_blob(geom.xy, camera_params)
// Render directly instead of pushing to commands
renderer.geometry_raw(gpu, transformed_xy, geom.xy_stride, geom.color, geom.color_stride, geom.uv, geom.uv_stride, geom.num_vertices, geom.indices, geom.num_indices, geom.size_indices)
break
}
})
return renderer_commands
}
///// input /////
var input_cb
var input_rate = 1/60
function poll_input() {
var evs = input.get_events()
// Filter and transform events
if (renderer && Array.isArray(evs)) {
var filteredEvents = []
var wantMouse = imgui.wantmouse()
var wantKeys = imgui.wantkeys()
for (var i = 0; i < evs.length; i++) {
var event = evs[i]
var shouldInclude = true
// Filter mouse events if ImGui wants mouse input
if (wantMouse && (event.type == 'mouse_motion' ||
event.type == 'mouse_button_down' ||
event.type == 'mouse_button_up' ||
event.type == 'mouse_wheel')) {
shouldInclude = false
}
// Filter keyboard events if ImGui wants keyboard input
if (wantKeys && (event.type == 'key_down' ||
event.type == 'key_up' ||
event.type == 'text_input' ||
event.type == 'text_editing')) {
shouldInclude = false
}
if (shouldInclude) {
// Transform mouse coordinates from window to renderer coordinates
if (event.pos && (event.type == 'mouse_motion' ||
event.type == 'mouse_button_down' ||
event.type == 'mouse_button_up' ||
event.type == 'mouse_wheel')) {
// Convert window coordinates to renderer logical coordinates
var logicalPos = renderer.coordsFromWindow(event.pos)
event.pos = logicalPos
}
// Handle drop events which also have position
if (event.pos && (event.type == 'drop_file' ||
event.type == 'drop_text' ||
event.type == 'drop_position')) {
var logicalPos = renderer.coordsFromWindow(event.pos)
event.pos = logicalPos
}
// Handle window events
if (event.type == 'window_pixel_size_changed') {
win_size.width = event.width
win_size.height = event.height
}
if (event.type == 'quit')
$_.stop()
if (event.type.includes('key')) {
if (event.key)
event.key = input.keyname(event.key)
}
if (event.type.startsWith('mouse_') && event.pos && event.pos.y)
event.pos.y = -event.pos.y + logical.height
filteredEvents.push(event)
}
}
evs = filteredEvents
}
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) {
renderer.drawColor = {r:0.1,g:0.1,b:0.15,a:1}
renderer.clear()
if (draw_cmds && draw_cmds.length)
var commands = translate_draw_commands(draw_cmds)
renderer.drawColor = {r:1,g:1,b:1,a:1}
imgui.endframe(renderer)
imgui.newframe()
renderer.present()
if (done) 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) {
window.size = size
},
}
prosperon.set_window = function(config)
{
for (var c in config)
if (window_cmds[c]) window_cmds[c](config[c])
}
var renderer_cmds = {
resolution(e) {
logical.width = e.width
logical.height = e.height
renderer.logicalPresentation = {...e}
}
}
prosperon.set_renderer = function(config)
{
for (var c in config)
if (renderer_cmds[c]) renderer_cmds[c](config[c])
}
prosperon.init = function() {
// No longer needed since we initialize directly
}
// Function to load textures directly to the renderer
prosperon.load_texture = function(surface_data) {
var surf = new surface(surface_data)
if (!surf) return null
var tex = renderer.load_texture(surf)
if (!tex) return null
// Set pixel mode to nearest for all textures
tex.scaleMode = "nearest"
var tex_id = allocate_id()
resources.texture[tex_id] = tex
return {
id: tex_id,
texture: tex,
width: tex.width,
height: tex.height
}
}
return prosperon