This commit is contained in:
2026-01-06 20:25:55 -06:00
parent 50bee7a5c0
commit 0522b967ca
21 changed files with 724 additions and 1244 deletions

10
clay.cm
View File

@@ -13,15 +13,15 @@ var CHILDREN = 'children'
var PARENT = 'parent'
function normalizeSpacing(spacing) {
if (typeof spacing == 'number') {
if (is_number(spacing)) {
return {l: spacing, r: spacing, t: spacing, b: spacing}
} else if (isa(spacing, array)) {
} else if (is_array(spacing)) {
if (spacing.length == 2) {
return {l: spacing[0], r: spacing[0], t: spacing[1], b: spacing[1]}
} else if (spacing.length == 4) {
return {l: spacing[0], r: spacing[1], t: spacing[2], b: spacing[3]}
}
} else if (typeof spacing == 'object') {
} else if (is_object(spacing)) {
return {l: spacing.l || 0, r: spacing.r || 0, t: spacing.t || 0, b: spacing.b || 0}
} else {
return {l:0, r:0, t:0, b:0}
@@ -66,7 +66,7 @@ clay.draw = function draw(fn, size = [prosperon.camera.width, prosperon.camera.h
lay_ctx.reset();
var root = lay_ctx.item();
// Accept both array and object formats
if (isa(size, array)) {
if (is_array(size)) {
size = {width: size[0], height: size[1]};
}
lay_ctx.set_size(root,size);
@@ -225,7 +225,7 @@ function add_item(config)
lay_ctx.set_margins(item, use_config.margin);
use_config.size ??= {width:0, height:0}
// Convert array to object if needed
if (isa(use_config.size, array)) {
if (is_array(use_config.size)) {
use_config.size = {width: use_config.size[0], height: use_config.size[1]};
}
// Apply max_size constraint - only clamp computed size, don't set explicit size pre-layout

View File

@@ -38,12 +38,12 @@ var base_config = {
}
function normalize_spacing(s) {
if (typeof s == 'number') return {l:s, r:s, t:s, b:s}
if (isa(s, array)) {
if (is_number(s)) return {l:s, r:s, t:s, b:s}
if (is_array(s)) {
if (s.length == 2) return {l:s[0], r:s[0], t:s[1], b:s[1]}
if (s.length == 4) return {l:s[0], r:s[1], t:s[2], b:s[3]}
}
if (typeof s == 'object') return {l:s.l||0, r:s.r||0, t:s.t||0, b:s.b||0}
if (is_object(s)) return {l:s.l||0, r:s.r||0, t:s.t||0, b:s.b||0}
return {l:0, r:0, t:0, b:0}
}
@@ -58,7 +58,7 @@ var tree_stack = []
clay.layout = function(fn, size) {
lay_ctx.reset()
var root_id = lay_ctx.item()
if (isa(size, array)) size = {width: size[0], height: size[1]}
if (is_array(size)) size = {width: size[0], height: size[1]}
lay_ctx.set_size(root_id, size)
lay_ctx.set_contain(root_id, layout.contain.row)
@@ -235,7 +235,7 @@ function push_node(configs, contain_mode) {
if (config.size) {
var s = config.size
if (isa(s, array)) s = {width: s[0], height: s[1]}
if (is_array(s)) s = {width: s[0], height: s[1]}
lay_ctx.set_size(item, s)
}
@@ -260,8 +260,8 @@ function pop_node() {
// Generic container
clay.container = function(configs, fn) {
if (typeof configs == 'function') { fn = configs; configs = {} }
if (!isa(configs, array)) configs = [configs]
if (is_function(configs)) { fn = configs; configs = {} }
if (!is_array(configs)) configs = [configs]
push_node(configs, null)
if (fn) fn()
@@ -270,8 +270,8 @@ clay.container = function(configs, fn) {
// Stacks
clay.vstack = function(configs, fn) {
if (typeof configs == 'function') { fn = configs; configs = {} }
if (!isa(configs, array)) configs = [configs]
if (is_function(configs)) { fn = configs; configs = {} }
if (!is_array(configs)) configs = [configs]
var c = layout.contain.column
@@ -281,8 +281,8 @@ clay.vstack = function(configs, fn) {
}
clay.hstack = function(configs, fn) {
if (typeof configs == 'function') { fn = configs; configs = {} }
if (!isa(configs, array)) configs = [configs]
if (is_function(configs)) { fn = configs; configs = {} }
if (!is_array(configs)) configs = [configs]
var c = layout.contain.row
push_node(configs, c)
@@ -291,8 +291,8 @@ clay.hstack = function(configs, fn) {
}
clay.zstack = function(configs, fn) {
if (typeof configs == 'function') { fn = configs; configs = {} }
if (!isa(configs, array)) configs = [configs]
if (is_function(configs)) { fn = configs; configs = {} }
if (!is_array(configs)) configs = [configs]
var c = layout.contain.layout

View File

@@ -98,8 +98,8 @@ Color.normalize = function (c) {
};
for (var p of array(c)) {
if (typeof c[p] != "object") continue;
if (!isa(c[p], array)) {
if (!is_object(c[p])) continue;
if (!is_array(c[p])) {
Color.normalize(c[p]);
continue;
}

File diff suppressed because it is too large Load Diff

View File

@@ -108,8 +108,8 @@ input.mouse.normal.doc = "Set the mouse to show again after hiding.";
input.keyboard = {};
input.keyboard.down = function (code) {
if (typeof code == "number") return downkeys[code];
if (typeof code == "string") return downkeys[code.toUpperCase().charCodeAt()] || downkeys[code.toLowerCase().charCodeAt()];
if (is_number(code)) return downkeys[code];
if (is_text(code)) return downkeys[code.toUpperCase().charCodeAt()] || downkeys[code.toLowerCase().charCodeAt()];
return null;
};
@@ -158,7 +158,7 @@ input.print_md_kbm = function print_md_kbm(pawn) {
};
input.has_bind = function (pawn, bind) {
return typeof pawn.inputs?.[bind] == "function";
return is_function(pawn.inputs?.[bind]);
};
input.action = {
@@ -214,7 +214,7 @@ var Player = {
mouse_input(type, ...args) {
for (var pawn of [...this.pawns].reverse()) {
if (typeof pawn.inputs?.mouse?.[type] == "function") {
if (is_function(pawn.inputs?.mouse?.[type])) {
pawn.inputs.mouse[type].call(pawn, ...args);
pawn.inputs.post?.call(pawn);
if (!pawn.inputs.fallthru) return;
@@ -224,7 +224,7 @@ var Player = {
char_input(c) {
for (var pawn of [...this.pawns].reverse()) {
if (typeof pawn.inputs?.char == "function") {
if (is_function(pawn.inputs?.char)) {
pawn.inputs.char.call(pawn, c);
pawn.inputs.post?.call(pawn);
if (!pawn.inputs.fallthru) return;
@@ -271,12 +271,12 @@ var Player = {
fn = inputs[cmd].released;
break;
case "down":
if (typeof inputs[cmd].down == "function") fn = inputs[cmd].down;
if (is_function(inputs[cmd].down)) fn = inputs[cmd].down;
else if (inputs[cmd].down) fn = inputs[cmd];
}
var consumed = false;
if (typeof fn == "function") {
if (is_function(fn)) {
fn.call(pawn, ...args);
consumed = true;
}

View File

@@ -110,7 +110,7 @@ function _render_node_tree(imgui, node, depth) {
var label = type + " [" + id + "]"
// Add dirty indicator
if (isa(node.dirty, number) && node.dirty > 0) {
if (is_number(node.dirty) && node.dirty > 0) {
label += " *"
}
@@ -249,7 +249,7 @@ function _render_node_inspector(imgui, node) {
for (var k in fx) {
if (k != 'type' && k != 'source') {
var v = fx[k]
if (typeof v == 'number') {
if (is_number(v)) {
fx[k] = imgui.slider(k, v, 0, 10)
} else {
imgui.text(k + ": " + text(v))

View File

@@ -180,7 +180,7 @@ draw.grid = function grid(rect, spacing, thickness = 1, offset = {x: 0, y: 0}, m
rect.width == null || rect.height == null) {
throw Error('Grid requires rect with x, y, width, height')
}
if (!spacing || typeof spacing.x == 'undefined' || typeof spacing.y == 'undefined') {
if (!is_object(spacing)|| is_null(spacing.x) || is_null(spacing.y)) {
throw Error('Grid requires spacing with x and y')
}

View File

@@ -92,7 +92,8 @@ effects.register('mask', {
type: 'conditional',
requires_target: true,
params: {
source: {required: true},
source: {required: false}, // Legacy: direct handle reference
source_id: {required: false}, // New: ID string for film2d.get()
channel: {default: 'alpha'},
invert: {default: false},
soft: {default: false},
@@ -112,11 +113,22 @@ effects.register('mask', {
return [{type: 'blit', source: input, dest: output}]
}
// Resolve mask source
var mask_source = params.source
if (params.source_id && ctx.film2d) {
mask_source = ctx.film2d.get(params.source_id)
}
if (!mask_source) {
// No mask source - pass through
return [{type: 'blit', source: input, dest: output}]
}
// Render mask source to target
var mask_target = ctx.alloc_target(size.width, size.height, 'mask_src')
passes.push({
type: 'render_subtree',
root: params.source,
root: mask_source,
output: mask_target,
clear: {r: 0, g: 0, b: 0, a: 0},
space: params.space || 'local'

551
film2d.cm
View File

@@ -1,462 +1,185 @@
// film2d.cm - 2D Scene Renderer (Rewritten)
//
// Handles scene tree traversal, sorting, batching, and draw command emission.
// This is the "how to draw a 2D plate" module.
//
// The compositor calls this renderer with (root, camera, target, target_size)
// and it produces draw commands.
//
// This module does NOT handle effects - that's compositor territory.
// It only knows about sprites, tilemaps, text, particles, and rects.
var film2d = {}
// Renderer capabilities
film2d.capabilities = {
supports_mask_stencil: true,
supports_mask_alpha: true,
supports_bloom: true,
supports_blur: true
}
var next_id = 1
var registry = {} // id -> drawable
var group_index = {} // group_name -> [id, id, ...]
// ------------------------------------------------------------------------
// Handle Registry
// ------------------------------------------------------------------------
var _next_handle_id = 1
var _handles = {}
function _create_handle(type, props) {
var id = _next_handle_id++
var handle = {
_id: id,
_gen: 1,
type: type,
active: true,
// Core Transform
x: props.pos ? props.pos.x : 0,
y: props.pos ? props.pos.y : 0,
rotation: props.rotation || 0,
scale_x: props.scale ? (props.scale.x || props.scale) : 1,
scale_y: props.scale ? (props.scale.y || props.scale) : 1,
anchor_x: props.anchor_x || 0,
anchor_y: props.anchor_y || 0,
// Properties
layer: props.layer || 0,
color: props.color || {r:1, g:1, b:1, a:1},
visible: true,
tags: props.tags || [],
// Methods
set_pos: function(x, y) { this.x = x; this.y = y; return this },
set_scale: function(x, y) { this.scale_x = x; this.scale_y = (y == null ? x : y); return this },
set_rotation: function(r) { this.rotation = r; return this },
set_layer: function(l) { this.layer = l; return this },
set_color: function(r, g, b, a) {
if (typeof r == 'object') { this.color = r }
else { this.color = {r:r, g:g, b:b, a:a} }
return this
},
set_visible: function(v) { this.visible = v; return this },
add_tag: function(t) { if (this.tags.indexOf(t) < 0) this.tags.push(t); return this },
remove_tag: function(t) {
var idx = this.tags.indexOf(t)
if (idx >= 0) this.tags.splice(idx, 1)
return this
},
has_tag: function(t) { return this.tags.indexOf(t) >= 0 },
destroy: function() { this.active = false; delete _handles[this._id] }
film2d.register = function(drawable) {
var id = text(next_id++)
drawable._id = id
registry[id] = drawable
var groups = drawable.groups || ['default']
for (var i = 0; i < groups.length; i++) {
var g = groups[i]
if (!group_index[g]) group_index[g] = []
group_index[g].push(id)
}
// Type specific properties
if (type == 'sprite') {
handle.image = props.image
handle.width = props.width || 0
handle.height = props.height || 0
handle.material = props.material
handle.slice = props.slice
handle.tile = props.tile
} else if (type == 'text') {
handle.text = props.text
handle.font = props.font
handle.size = props.size
handle.mode = props.mode
handle.outline_width = props.outline_width
handle.outline_color = props.outline_color
} else if (type == 'particles') {
handle.particles = props.particles || []
handle.image = props.image
handle.width = props.width
handle.height = props.height
handle.material = props.material
} else if (type == 'tilemap') {
handle.tiles = props.tiles || []
handle.offset_x = props.offset_x || 0
handle.offset_y = props.offset_y || 0
handle.scale_tile_x = props.scale_x || 1 // avoid naming conflict with transform scale
handle.scale_tile_y = props.scale_y || 1
handle.material = props.material
return id
}
film2d.unregister = function(id) {
var id_str = text(id)
var drawable = registry[id_str]
if (!drawable) return
var groups = drawable.groups || []
for (var i = 0; i < groups.length; i++) {
var g = groups[i]
if (group_index[g]) {
var idx = group_index[g].indexOf(id_str)
if (idx >= 0) group_index[g].splice(idx, 1)
}
}
_handles[id] = handle
return handle
delete registry[id_str]
}
film2d.create_sprite = function(props) { return _create_handle('sprite', props) }
film2d.create_text = function(props) { return _create_handle('text', props) }
film2d.create_particles = function(props) { return _create_handle('particles', props) }
film2d.create_tilemap = function(props) { return _create_handle('tilemap', props) }
// Support for creating raw texture refs (usually internal, but exposed)
film2d.create_texture_ref = function(props) {
// Texture refs are usually transient drawables, but we can make a handle
return _create_handle('texture_ref', props)
film2d.index_group = function(id, group) {
if (!group_index[group]) group_index[group] = []
if (group_index[group].indexOf(text(id)) < 0)
group_index[group].push(text(id))
}
// ------------------------------------------------------------------------
// Render Pipeline
// ------------------------------------------------------------------------
film2d.unindex_group = function(id, group) {
if (!group_index[group]) return
var idx = group_index[group].indexOf(text(id))
if (idx >= 0) group_index[group].splice(idx, 1)
}
// Main entrypoint: render({ drawables, camera, target, ... })
// No root tree.
film2d.reindex = function(id, old_groups, new_groups) {
for (var i = 0; i < old_groups.length; i++)
film2d.unindex_group(id, old_groups[i])
for (var i = 0; i < new_groups.length; i++)
film2d.index_group(id, new_groups[i])
}
film2d.get = function(id) {
return registry[text(id)]
}
// Query by group - returns array of drawables
film2d.query = function(selector) {
var result = []
if (selector.group) {
var ids = group_index[selector.group] || []
for (var i = 0; i < ids.length; i++) {
var d = registry[ids[i]]
if (d && d.visible != false) result.push(d)
}
return result
}
if (selector.groups) {
var seen = {}
for (var g = 0; g < selector.groups.length; g++) {
var ids = group_index[selector.groups[g]] || []
for (var i = 0; i < ids.length; i++) {
if (!seen[ids[i]]) {
seen[ids[i]] = true
var d = registry[ids[i]]
if (d && d.visible != false) result.push(d)
}
}
}
return result
}
// All drawables
for (var id in registry) {
var d = registry[id]
if (d && d.visible != false) result.push(d)
}
return result
}
// Get all groups a drawable belongs to
film2d.get_groups = function(id) {
var d = registry[text(id)]
return d ? (d.groups || []) : []
}
// List all known groups
film2d.all_groups = function() {
var groups = []
for (var g in group_index)
if (group_index[g].length > 0) groups.push(g)
return groups
}
// Render function - takes drawables directly, no tree traversal
film2d.render = function(params, backend) {
var drawables_in = params.drawables || []
var drawables = params.drawables || []
var camera = params.camera
var target = params.target
var target_size = params.target_size
var clear_color = params.clear
if (drawables_in.length == 0) return {commands: []}
if (drawables.length == 0) return {commands: []}
// flatten and resolve handles
var resolved_drawables = _resolve_and_flatten(drawables_in)
// Sort by layer, then by Y
resolved_drawables.sort(function(a, b) {
var difflayer = a.layer - b.layer
if (difflayer != 0) return difflayer
return b.world_y - a.world_y
// Sort by layer, then Y
drawables.sort(function(a, b) {
var dl = (a.layer || 0) - (b.layer || 0)
if (dl != 0) return dl
return (b.pos.y || 0) - (a.pos.y || 0)
})
// Build render commands
var commands = []
commands.push({cmd: 'begin_render', target: target, clear: clear_color})
// Apply camera?
// User didn't specify if film2d owns camera application or if it's baked.
// Existing film2d used 'set_camera' command. preserving that.
commands.push({cmd: 'set_camera', camera: camera})
// Batch and emit
var batches = _batch_drawables(resolved_drawables)
var current_scissor = null
var batches = _batch_drawables(drawables)
for (var i = 0; i < batches.length; i++) {
var batch = batches[i]
// Emit scissor if changed
if (!_rect_equal(current_scissor, batch.scissor)) {
commands.push({cmd: 'scissor', rect: batch.scissor})
current_scissor = batch.scissor
}
if (batch.type == 'sprite_batch') {
commands.push({
cmd: 'draw_batch',
batch_type: 'sprites',
geometry: {sprites: batch.sprites},
texture: batch.texture,
material: batch.material
})
} else if (batch.type == 'text') {
commands.push({
cmd: 'draw_text',
drawable: batch.drawable
})
} else if (batch.type == 'rect') {
commands.push({
cmd: 'draw_rect',
drawable: batch.drawable
})
} else if (batch.type == 'particles') {
commands.push({
cmd: 'draw_batch',
batch_type: 'particles',
geometry: {sprites: batch.sprites},
texture: batch.texture,
material: batch.material
})
} else if (batch.type == 'texture_ref') {
commands.push({
cmd: 'draw_texture_ref',
drawable: batch.drawable
})
}
if (batch.type == 'sprite_batch')
commands.push({cmd: 'draw_batch', batch_type: 'sprites', geometry: {sprites: batch.sprites}, texture: batch.texture, material: batch.material})
else if (batch.type == 'text')
commands.push({cmd: 'draw_text', drawable: batch.drawable})
else if (batch.type == 'texture_ref')
commands.push({cmd: 'draw_texture_ref', drawable: batch.drawable})
}
commands.push({cmd: 'end_render'})
return {target: target, commands: commands}
return {commands: commands}
}
// Convert input list (handles or structs) into flat list of primitive drawables
function _resolve_and_flatten(inputs) {
var out = []
for (var i = 0; i < inputs.length; i++) {
var item = inputs[i]
if (!item) continue
// If it's a handle (has _id), use it. If it's a raw struct, use it.
// Handles are already mostly "drawable-like", but tilemaps need expansion.
if (item.type == 'tilemap') {
// Tilemaps expand to many sprites
_expand_tilemap(item, out)
} else if (item.type == 'sprite') {
_expand_sprite(item, out)
} else if (item.type == 'text' || item.type == 'texture_ref' || item.type == 'particles' || item.type == 'rect') {
// Pass through (maybe copy needed if we mutate for batching?)
// We need 'world_y' for sorting.
// Ensure pos is world pos.
// If item is a handle, it has x/y which are world x/y in flat model.
var d = _clone_for_render(item)
d.world_y = d.pos.y
out.push(d)
}
}
return out
}
function _clone_for_render(item) {
// Start with a shallow copy or extraction of render properties
var d = {
type: item.type,
layer: item.layer,
pos: {x: item.x != null ? item.x : item.pos.x, y: item.y != null ? item.y : item.pos.y},
color: item.color,
scissor: item.scissor,
// specific props
text: item.text,
font: item.font,
size: item.size,
mode: item.mode,
sdf: item.sdf,
outline_width: item.outline_width,
outline_color: item.outline_color,
anchor_x: item.anchor_x,
anchor_y: item.anchor_y,
width: item.width,
height: item.height,
texture_target: item.texture_target,
blend: item.blend,
particles: item.particles,
image: item.image,
texture: item.texture,
material: item.material
}
return d
}
function _expand_sprite(node, out) {
var x = node.x != null ? node.x : (node.pos ? node.pos.x : 0)
var y = node.y != null ? node.y : (node.pos ? node.pos.y : 0)
var w = node.width || 1
var h = node.height || 1
var ax = node.anchor_x || 0
var ay = node.anchor_y || 0
var color = node.color || {r:1,g:1,b:1,a:1}
var layer = node.layer || 0
var scissor = node.scissor
var x0 = x - w * ax
var y0 = y - h * ay
if (node.slice) {
// 9-slice expansion
var s = node.slice
var L = s.left != null ? s.left : (typeof s == 'number' ? s : 0)
var R = s.right != null ? s.right : (typeof s == 'number' ? s : 0)
var T = s.top != null ? s.top : (typeof s == 'number' ? s : 0)
var B = s.bottom != null ? s.bottom : (typeof s == 'number' ? s : 0)
var stretch = s.stretch != null ? s.stretch : node.stretch
var Sx = stretch != null ? (stretch.x || stretch) : w
var Sy = stretch != null ? (stretch.y || stretch) : h
var WL = L * Sx
var WR = R * Sx
var HT = T * Sy
var HB = B * Sy
var WM = w - WL - WR
var HM = h - HT - HB
var UM = 1 - L - R
var VM = 1 - T - B
var TW = stretch != null ? UM * Sx : WM
var TH = stretch != null ? VM * Sy : HM
function add(rect, uv) {
out.push({
type: 'sprite', layer: layer, world_y: y, pos: {x: rect.x, y: rect.y},
image: node.image, texture: node.texture, material: node.material,
width: rect.width, height: rect.height, anchor_x: 0, anchor_y: 0,
uv_rect: uv, color: color, scissor: scissor
})
}
function tiled(rect, uv, tile_size) {
var tx = tile_size ? (tile_size.x || tile_size) : rect.width
var ty = tile_size ? (tile_size.y || tile_size) : rect.height
var nx = number.ceiling(rect.width / tx - 0.00001)
var ny = number.ceiling(rect.height / ty - 0.00001)
for (var ix = 0; ix < nx; ix++) {
for (var iy = 0; iy < ny; iy++) {
var qw = number.min(tx, rect.width - ix * tx)
var qh = number.min(ty, rect.height - iy * ty)
var quv = {x: uv.x, y: uv.y, width: uv.width * (qw/tx), height: uv.height * (qh/ty)}
add({x: rect.x + ix*tx, y: rect.y + iy*ty, width: qw, height: qh}, quv)
}
}
}
// TL, TM, TR
add({x: x0, y: y0, width: WL, height: HT}, {x:0, y:0, width: L, height: T})
tiled({x: x0 + WL, y: y0, width: WM, height: HT}, {x:L, y:0, width: UM, height: T}, {x: TW, y: HT})
add({x: x0 + WL + WM, y: y0, width: WR, height: HT}, {x: 1-R, y:0, width: R, height: T})
// ML, MM, MR
tiled({x: x0, y: y0 + HT, width: WL, height: HM}, {x:0, y:T, width: L, height: VM}, {x: WL, y: TH})
tiled({x: x0 + WL, y: y0 + HT, width: WM, height: HM}, {x:L, y:T, width: UM, height: VM}, {x: TW, y: TH})
tiled({x: x0 + WL + WM, y: y0 + HT, width: WR, height: HM}, {x: 1-R, y:T, width: R, height: VM}, {x: WR, y: TH})
// BL, BM, BR
add({x: x0, y: y0 + HT + HM, width: WL, height: HB}, {x:0, y: 1-B, width: L, height: B})
tiled({x: x0 + WL, y: y0 + HT + HM, width: WM, height: HB}, {x:L, y: 1-B, width: UM, height: B}, {x: TW, y: HB})
add({x: x0 + WL + WM, y: y0 + HT + HM, width: WR, height: HB}, {x: 1-R, y: 1-B, width: R, height: B})
} else if (node.tile) {
// Tiled mode logic ... simplified: assume pre-expanded in batched logic?
// Actually batch logic handles simple sprites. We need to expand here.
var tx = node.tile.x || node.tile
var ty = node.tile.y || node.tile // if just number
if (typeof node.tile == 'number') { tx = node.tile; ty = node.tile }
var nx = number.ceiling(w / tx - 0.00001)
var ny = number.ceiling(h / ty - 0.00001)
for (var ix = 0; ix < nx; ix++) {
for (var iy = 0; iy < ny; iy++) {
var qw = number.min(tx, w - ix * tx)
var qh = number.min(ty, h - iy * ty)
out.push({
type: 'sprite', layer: layer, world_y: y, pos: {x: x0 + ix*tx, y: y0 + iy*ty},
image: node.image, texture: node.texture, material: node.material,
width: qw, height: qh, anchor_x: 0, anchor_y: 0,
// uv logic for detailed tiling omitted for brevity but should be here
// assuming simple repeat
uv_rect: {x:0, y:0, width: qw/tx, height: qh/ty},
color: color, scissor: scissor
})
}
}
} else {
// Normal sprite
out.push(_clone_for_render(node))
}
}
function _expand_tilemap(node, out) {
var tiles = node.tiles || []
var x = node.x || 0
var y = node.y || 0
var offset_x = node.offset_x || 0
var offset_y = node.offset_y || 0
var scale_x = node.scale_tile_x || node.scale_x || 1
var scale_y = node.scale_tile_y || node.scale_y || 1
var color = node.color || {r:1,g:1,b:1,a:1}
for (var ix = 0; ix < tiles.length; ix++) {
if (!tiles[ix]) continue
for (var iy = 0; iy < tiles[ix].length; iy++) {
var tile = tiles[ix][iy]
if (!tile) continue
var world_x = x + (ix + offset_x) * scale_x
var world_y = y + (iy + offset_y) * scale_y
out.push({
type: 'sprite',
layer: node.layer || 0,
world_y: world_y,
pos: {x: world_x, y: world_y},
image: tile,
texture: tile,
width: scale_x,
height: scale_y,
anchor_x: 0,
anchor_y: 0,
color: color,
material: node.material,
scissor: node.scissor
})
}
}
}
// Batch drawables for efficient rendering
function _batch_drawables(drawables) {
var batches = []
var current_batch = null
var mat = {blend: 'alpha', sampler: 'nearest'}
var current = null
var default_mat = {blend: 'alpha', sampler: 'nearest'}
array.for(drawables, drawable => {
if (drawable.type == 'sprite') {
var texture = drawable.texture || drawable.image
var material = drawable.material || mat
var scissor = drawable.scissor
for (var i = 0; i < drawables.length; i++) {
var d = drawables[i]
if (d.type == 'sprite') {
var tex = d.texture || d.image
var mat = d.material || default_mat
// Check if can merge with current batch
if (current_batch &&
current_batch.type == 'sprite_batch' &&
current_batch.texture == texture &&
_rect_equal(current_batch.scissor, scissor) &&
_materials_equal(current_batch.material, material)) {
current_batch.sprites.push(drawable)
if (current && current.type == 'sprite_batch' && current.texture == tex && _mat_eq(current.material, mat)) {
current.sprites.push(d)
} else {
if (current_batch) batches.push(current_batch)
current_batch = {
type: 'sprite_batch',
texture: texture,
material: material,
scissor: scissor,
sprites: [drawable]
}
if (current) batches.push(current)
current = {type: 'sprite_batch', texture: tex, material: mat, sprites: [d]}
}
} else {
// Non-sprite: flush batch, add individually
if (current_batch) {
batches.push(current_batch)
current_batch = null
if (current) {
batches.push(current)
current = null
}
batches.push({type: drawable.type, drawable: drawable, scissor: drawable.scissor})
batches.push({type: d.type, drawable: d})
}
})
if (current_batch) batches.push(current_batch)
}
if (current) batches.push(current)
return batches
}
function _rect_equal(a, b) {
if (!a && !b) return true
if (!a || !b) return false
return a.x == b.x && a.y == b.y && a.width == b.width && a.height == b.height
}
function _materials_equal(a, b) {
function _mat_eq(a, b) {
if (!a || !b) return a == b
return a.blend == b.blend && a.sampler == b.sampler && a.shader == b.shader
return a.blend == b.blend && a.sampler == b.sampler
}
return film2d
return film2d

View File

@@ -161,12 +161,12 @@ function create_image(path){
var raw = decode_image(bytes, ext);
/* ── Case A: single surface (from make_texture) ────────────── */
if(raw && raw.width && raw.pixels && !isa(raw, array)) {
if(raw && raw.width && raw.pixels && !is_array(raw)) {
return new graphics.Image(raw)
}
/* ── Case B: array of surfaces (from make_gif) ────────────── */
if(isa(raw, array)) {
if(is_array(raw)) {
// Single frame GIF returns array with one surface
if(raw.length == 1 && !raw[0].time) {
return new graphics.Image(raw[0])
@@ -175,16 +175,16 @@ function create_image(path){
return makeAnim(wrapFrames(raw), true);
}
if(typeof raw == 'object' && !raw.width) {
if(is_object(raw) && !raw.width) {
if(raw.surface)
return new graphics.Image(raw.surface)
if(raw.frames && isa(raw.frames, array) && raw.loop != null)
if(raw.frames && is_array(raw.frames) && raw.loop != null)
return makeAnim(wrapFrames(raw.frames), !!raw.loop);
def anims = {};
for(def [name, anim] of Object.entries(raw)){
if(anim && isa(anim.frames, array))
if(anim && is_array(anim.frames))
anims[name] = makeAnim(wrapFrames(anim.frames), !!anim.loop);
else if(anim && anim.surface)
anims[name] = new graphics.Image(anim.surface);
@@ -250,7 +250,7 @@ graphics.from_surface = function(surf)
graphics.from = function(id, data)
{
if (typeof id != 'string')
if (!is_text(id))
throw new Error('Expected a string ID')
if (data instanceof ArrayBuffer)
@@ -260,7 +260,7 @@ graphics.from = function(id, data)
graphics.texture = function texture(path) {
if (path instanceof graphics.Image) return path
if (typeof path != 'string')
if (!is_text(path))
throw new Error('need a string for graphics.texture')
var parts = path.split(':')
@@ -269,7 +269,7 @@ graphics.texture = function texture(path) {
var frameIndex = parts[2]
// Handle the case where animName is actually a frame index (e.g., "gears:0")
if (animName != null && frameIndex == null && !isa(number(animName), null)) {
if (animName != null && frameIndex == null && !is_null(number(animName))) {
frameIndex = number(animName)
animName = null
}
@@ -294,7 +294,7 @@ graphics.texture = function texture(path) {
// Handle frame index without animation name (e.g., "gears:0")
if (!animName && frameIndex != null) {
// If cached is a single animation (has .frames property)
if (cached.frames && isa(cached.frames, array)) {
if (cached.frames && is_array(cached.frames)) {
var idx = number(frameIndex)
if (idx == null) return cached
// Wrap the index
@@ -318,7 +318,7 @@ graphics.texture = function texture(path) {
}
// If cached is a single animation (has .frames property)
if (cached.frames && isa(cached.frames, array)) {
if (cached.frames && is_array(cached.frames)) {
if (frameIndex != null) {
var idx = number(frameIndex)
if (isNaN(idx)) return cached
@@ -331,7 +331,7 @@ graphics.texture = function texture(path) {
}
// If cached is an object of multiple animations
if (typeof cached == 'object' && !cached.frames) {
if (is_object(cached) && !cached.frames) {
var anim = cached[animName]
if (!anim)
throw new Error(`animation ${animName} not found in ${id}`)
@@ -343,7 +343,7 @@ graphics.texture = function texture(path) {
if (anim instanceof graphics.Image) {
// Single image animation - any frame index returns the image
return anim
} else if (anim.frames && isa(anim.frames, array)) {
} else if (anim.frames && is_array(anim.frames)) {
// Multi-frame animation - wrap the index
idx = idx % anim.frames.length
return anim.frames[idx].image
@@ -412,8 +412,8 @@ var fontcache = {}
var datas = []
graphics.get_font = function get_font(path) {
if (isa(path, object)) return path
if (!isa(path, text))
if (is_object(path)) return path
if (!is_text(path))
throw new Error(`Can't find font with path: ${path}`)
var parts = path.split('.')

View File

@@ -1 +1,5 @@
// particles
// particles
return function() {
}

View File

@@ -1,5 +1,140 @@
// particles2d.cm - Particle system factory
//
// Creates particle data objects that register with film2d
// Also provides emitter update logic
var film2d = use('film2d')
var random = use('random').random
return function() {
var particles2d_proto = {
type: 'particles',
destroy: function() {
film2d.unregister(this._id)
}
}
}
// Emitter management
var emitters = {
// Spawn a particle for an emitter
spawn: function(emitter) {
emitter.particles.push({
pos: {
x: emitter.pos.x + (random() - 0.5) * emitter.spawn_area.width,
y: emitter.pos.y + (random() - 0.5) * emitter.spawn_area.height
},
velocity: {
x: emitter.velocity.x + (random() - 0.5) * emitter.velocity_var.x,
y: emitter.velocity.y + (random() - 0.5) * emitter.velocity_var.y
},
life: emitter.life,
time: 0,
max_scale: emitter.scale + (random() - 0.5) * emitter.scale_var,
scale: 0,
color: {
r: emitter.color.r,
g: emitter.color.g + (random() - 0.5) * 0.3,
b: emitter.color.b,
a: 1
}
})
},
// Update an emitter and its particles
update: function(emitter, dt) {
// Spawn new particles
if (emitter.rate > 0) {
emitter.spawn_timer = (emitter.spawn_timer || 0) + dt
var pp = 1 / emitter.rate
while (emitter.spawn_timer > pp) {
emitter.spawn_timer -= pp
emitters.spawn(emitter)
}
}
// Update existing particles
for (var i = emitter.particles.length - 1; i >= 0; i--) {
var p = emitter.particles[i]
p.time += dt
p.pos.x += p.velocity.x * dt
p.pos.y += p.velocity.y * dt
// Scale animation
var grow_for = emitter.grow_for || 0.3
var shrink_for = emitter.shrink_for || 0.5
if (p.time < grow_for) {
p.scale = lerp(0, p.max_scale, p.time / grow_for)
} else if (p.time > p.life - shrink_for) {
p.scale = lerp(0, p.max_scale, (p.life - p.time) / shrink_for)
} else {
p.scale = p.max_scale
}
// Alpha fade
var alpha = 1
if (p.time > p.life * 0.7) {
alpha = 1 - (p.time - p.life * 0.7) / (p.life * 0.3)
}
p.color.a = alpha
// Remove dead particles
if (p.time >= p.life) {
emitter.particles.splice(i, 1)
}
}
// Sync to film2d if handle provided
if (emitter.handle) {
emitter.handle.particles = emitter.particles
}
},
// Create an emitter config
create: function(config) {
return {
pos: config.pos || {x: 0, y: 0},
spawn_area: config.spawn_area || {width: 10, height: 10},
velocity: config.velocity || {x: 0, y: 100},
velocity_var: config.velocity_var || {x: 20, y: 20},
life: config.life || 2,
rate: config.rate || 10,
scale: config.scale || 1,
scale_var: config.scale_var || 0.3,
grow_for: config.grow_for || 0.3,
shrink_for: config.shrink_for || 0.5,
color: config.color || {r: 1, g: 1, b: 1, a: 1},
spawn_timer: 0,
particles: [],
handle: config.handle || null
}
}
}
function lerp(a, b, t) { return a + (b - a) * t }
// Factory function - auto-registers with film2d
var factory = function(props) {
var defaults = {
type: 'particles',
image: null,
width: 16,
height: 16,
plane: 'default',
layer: 0,
tags: [],
particles: []
}
var data = {}
for(var k in defaults) data[k] = defaults[k]
for(var k in props) data[k] = props[k]
var newparticles = meme(particles2d_proto, data)
film2d.register(newparticles)
return newparticles
}
// Attach emitter helpers to factory
factory.emitters = emitters
return factory

View File

@@ -329,9 +329,9 @@ function pack_ubo(obj, ubo_type, reflection) {
// Convert value to appropriate format based on type
if (member.type == "vec4") {
if (isa(value, array)) {
if (is_array(value)) {
result_blob.write_blob(geometry.array_blob(value));
} else if (typeof value == "object" && value.r != null) {
} else if (is_object(value) && value.r != null) {
// Color object
result_blob.write_blob(geometry.array_blob([value.r, value.g, value.b, value.a || 1]));
} else {
@@ -339,14 +339,14 @@ function pack_ubo(obj, ubo_type, reflection) {
result_blob.write_blob(geometry.array_blob([value, value, value, value]));
}
} else if (member.type == "vec3") {
if (isa(value, array)) {
if (is_array(value)) {
result_blob.write_blob(geometry.array_blob(value));
} else if (typeof value == 'object' && value.r != null)
} else if (is_object(value) && value.r != null)
result_blob.write_blob(geometry.array_blob([value.r, value.g, value.b]));
else
result_blob.write_blob(geometry.array_blob([value, value, value]));
} else if (member.type == "vec2") {
if (isa(value, array)) {
if (is_array(value)) {
result_blob.write_blob(geometry.array_blob(value));
} else {
result_blob.write_blob(geometry.array_blob([value, value]));
@@ -459,15 +459,15 @@ function pack_model_buffer(material) {
// Use material properties if available
if (material && material.model) {
if (isa(material.model, array) && material.model.length >= 16) {
if (is_array(material.model) && material.model.length >= 16) {
model_matrix = material.model.slice(0, 16);
}
}
if (material && material.color) {
if (isa(material.color, array) && material.color.length >= 4) {
if (is_array(material.color) && material.color.length >= 4) {
color = material.color.slice(0, 4);
} else if (typeof material.color == "object" && material.color.r != null) {
} else if (is_object(material.color) && material.color.r != null) {
color = [material.color.r, material.color.g, material.color.b, material.color.a || 1];
}
}
@@ -586,7 +586,7 @@ function poll_input() {
var evs = input.get_events()
// Filter and transform events
if (isa(evs, array)) {
if (is_array(evs)) {
var filteredEvents = []
// var wantMouse = imgui.wantmouse()
// var wantKeys = imgui.wantkeys()
@@ -779,7 +779,7 @@ function render_geom(geom, img, pipeline = get_pipeline_for_material(null), mate
cmd_fns.draw_image = function(cmd)
{
var img
if (typeof cmd.image == 'string')
if (is_text(cmd.image))
img = graphics.texture(cmd.image)
else
img = cmd.image
@@ -854,7 +854,7 @@ cmd_fns.tilemap = function(cmd)
cmd_fns.geometry = function(cmd)
{
var img
if (typeof cmd.image == 'object') {
if (is_object(cmd.image)) {
img = cmd.image
} else {
if (!cmd.image) return
@@ -886,7 +886,7 @@ cmd_fns.draw_slice9 = function(cmd)
// Convert single slice value to LRTB object if needed
var slice_lrtb = cmd.slice
if (typeof cmd.slice == 'number') {
if (is_number(cmd.slice)) {
var slice_val = cmd.slice
if (slice_val > 0 && slice_val < 1) {
slice_lrtb = {

View File

@@ -38,7 +38,7 @@ function isRecognizedExtension(ext) {
}
function find_in_path(filename, exts = []) {
if (typeof filename != 'string') return null
if (!is_text(filename)) return null
if (filename.includes('.')) {
var candidate = filename // possibly need "/" ?

View File

@@ -1027,7 +1027,7 @@ function _preload_textures(commands) {
for (var cmd of commands) {
if (cmd.cmd == 'draw_batch' && cmd.texture) {
if (typeof cmd.texture == 'string') {
if (is_text(cmd.texture)) {
paths[cmd.texture] = true
}
}

View File

@@ -1,85 +1,71 @@
// sprite.cm - Sprite node factory
//
// Returns a function that creates sprite instances via meme()
var film2d = use('film2d')
var dirty = {}
var sprite = {
// Setters that mark dirty
var sprite_proto = {
type: 'sprite',
set_pos: function(x, y) {
if (!this.pos) this.pos = {x: 0, y: 0}
if (this.pos.x == x && this.pos.y == y) return this
this.pos.x = x
this.pos.y = y
this.dirty |= 1 // TRANSFORM
return this
},
set_image: function(img) {
if (this.image == img) return this
this.image = img
this.dirty |= 2 // CONTENT
set_groups: function(groups) {
var old_groups = this.groups
this.groups = groups
film2d.reindex(this._id, old_groups, groups)
return this
},
set_size: function(w, h) {
if (this.width == w && this.height == h) return this
this.width = w
this.height = h
this.dirty |= 2 // CONTENT
add_group: function(group) {
if (this.groups.indexOf(group) < 0) {
this.groups.push(group)
film2d.index_group(this._id, group)
}
return this
},
set_anchor: function(x, y) {
if (this.anchor_x == x && this.anchor_y == y) return this
this.anchor_x = x
this.anchor_y = y
this.dirty |= 1 // TRANSFORM
remove_group: function(group) {
var idx = this.groups.indexOf(group)
if (idx >= 0) {
this.groups.splice(idx, 1)
film2d.unindex_group(this._id, group)
}
return this
},
set_color: function(color) {
if (!this.color) this.color = {r: 1, g: 1, b: 1, a: 1}
if (this.color.r == color.r && this.color.g == color.g && this.color.b == color.b && this.color.a == color.a) return this
this.color.r = color.r
this.color.g = color.g
this.color.b = color.b
this.color.a = a
this.dirty |= 2 // CONTENT
return this
},
set_opacity: function(o) {
if (this.opacity == o) return this
this.opacity = o
this.dirty |= 2 // CONTENT
return this
destroy: function() {
film2d.unregister(this._id)
}
}
stone(sprite)
// Factory function
return function(props) {
var defaults = {
pos: {x:0,y:0},
layer: 0,
type: 'sprite',
pos: {x: 0, y: 0},
image: null,
width: 1,
height: 1,
anchor_x: 0,
anchor_y: 0,
scale_x: 1,
scale_y: 1,
color: null,
uv_rect: null,
slice: null,
tile: null,
material: null,
flip_x: false,
flip_y: false,
rotation: 0,
color: {r: 1, g: 1, b: 1, a: 1},
opacity: 1,
layer: 0,
groups: ['default'],
visible: true
}
var newsprite = meme(sprite, [defaults,props])
newsprite[dirty] = 7
return newsprite
var data = {}
for (var k in defaults) data[k] = defaults[k]
for (var k in props) data[k] = props[k]
// Ensure groups is array
if (!data.groups) data.groups = ['default']
if (is_text(data.groups)) data.groups = [data.groups]
var s = meme(sprite_proto, data)
film2d.register(s)
return s
}

View File

@@ -1,14 +1,51 @@
var text2d = {
text: "",
pos: {x:0,y:0},
layer: 0,
font: "fonts/dos",
size: 16,
color: {r:1,g:1,b:1,a:1}
// text2d.cm - Text factory
//
// Creates text data objects that register with film2d
var film2d = use('film2d')
var text2d_proto = {
type: 'text',
set_text: function(t) {
this.text = t
return this
},
set_pos: function(x, y) {
this.pos.x = x
this.pos.y = y
return this
},
destroy: function() {
film2d.unregister(this._id)
}
}
stone(text2d)
// Factory function - auto-registers with film2d
return function(props) {
return meme(text2d, props)
var defaults = {
type: 'text',
text: "",
pos: {x: 0, y: 0},
plane: 'default',
layer: 0,
font: "fonts/dos",
size: 16,
color: {r: 1, g: 1, b: 1, a: 1},
mode: null,
sdf: null,
outline_width: null,
outline_color: null,
tags: []
}
var data = {}
for(var k in defaults) data[k] = defaults[k]
for(var k in props) data[k] = props[k]
var newtext = meme(text2d_proto, data)
film2d.register(newtext)
return newtext
}

View File

@@ -67,7 +67,7 @@ tilemap.prototype =
while (this.tiles.length <= x) this.tiles.push([]);
// Convert string to image object if needed, or handle null to remove tile
if (image && typeof image == 'string') {
if (image && is_text(image)) {
var graphics = use('graphics');
image = graphics.texture(image);
}

View File

@@ -1,16 +1,23 @@
// tilemap.js - MUCH SIMPLER
// tilemap2d.cm - Tilemap factory
//
// Creates tilemap data objects that register with film2d
var film2d = use('film2d')
var tilemap = {
at(pos) {
type: 'tilemap',
at: function(pos) {
var x = pos.x - this.offset_x
var y = pos.y - this.offset_y
if (!this.tiles[x]) return null
return this.tiles[x][y]
},
set(pos, image) {
set: function(pos, image) {
var x = pos.x - this.offset_x
var y = pos.y - this.offset_y
// Expand tiles array if needed
while (this.tiles.length <= x)
this.tiles.push([])
@@ -19,20 +26,34 @@ var tilemap = {
this.tiles[x][y] = image
},
destroy: function() {
film2d.unregister(this._id)
}
}
stone(tilemap)
// Factory function - auto-registers with film2d
return function(props) {
var defaults = {
type: 'tilemap',
tiles: [],
offset_x: 0,
offset_y: 0,
plane: 'default',
layer: 0,
tile_width: 1,
tile_height: 1,
tags: []
}
var newtilemap = meme(tilemap, [defaults,props])
newtilemap.tiles = []
var data = {}
for(var k in defaults) data[k] = defaults[k]
for(var k in props) data[k] = props[k]
var newtilemap = meme(tilemap, data)
newtilemap.tiles = [] // Initialize tiles
film2d.register(newtilemap)
return newtilemap
}

View File

@@ -41,7 +41,7 @@ function Tween(obj) {
Tween.prototype.to = function(props, duration, start_time) {
for (var key in props) {
var value = props[key]
if (isa(value, object)) {
if (is_object(value)) {
// Handle nested objects by flattening them
for (var subkey in value) {
var flatKey = key + '.' + subkey

View File

@@ -1,7 +1,11 @@
var world = {}
world.add_2d = function(sprite) {
this.render_2d.push(sprite)
}
return function() {
return meme(world)
var newworld = meme(world)
newworld.render_2d = []
return newworld
}