439 lines
11 KiB
Plaintext
439 lines
11 KiB
Plaintext
// clay.cm - UI layout engine emitting flat drawables with annotated tree
|
|
//
|
|
// - Uses meme/merge for config chains
|
|
// - Emits flat list of drawables for film2d
|
|
// - Supports scissor clipping
|
|
// - Returns annotated tree root with .drawables for clay_input compatibility
|
|
|
|
var layout = use('layout')
|
|
var graphics = use('graphics')
|
|
|
|
var clay = {}
|
|
|
|
// Unique key objects for tree traversal (used by clay_input)
|
|
var CHILDREN = {}
|
|
var PARENT = {}
|
|
clay.CHILDREN = CHILDREN
|
|
clay.PARENT = PARENT
|
|
|
|
// Layout context
|
|
var lay_ctx = layout.make_context()
|
|
|
|
// Base configuration for UI elements
|
|
var base_config = {
|
|
font: null,
|
|
background_image: null,
|
|
slice: 0,
|
|
font_path: 'fonts/dos', // Default font
|
|
font_size: 16,
|
|
color: {r:1, g:1, b:1, a:1},
|
|
spacing: 0,
|
|
padding: 0,
|
|
margin: 0,
|
|
offset: {x:0, y:0},
|
|
size: null,
|
|
background_color: null,
|
|
clipped: false,
|
|
text_break: 'word',
|
|
text_align: 'left',
|
|
max_size: null,
|
|
contain: 0,
|
|
behave: 0
|
|
}
|
|
|
|
function normalize_color(c, fallback) {
|
|
var fb = fallback || {r:1, g:1, b:1, a:1}
|
|
if (!c) return {r:fb.r, g:fb.g, b:fb.b, a:fb.a}
|
|
return {
|
|
r: c.r != null ? c.r : fb.r,
|
|
g: c.g != null ? c.g : fb.g,
|
|
b: c.b != null ? c.b : fb.b,
|
|
a: c.a != null ? c.a : fb.a
|
|
}
|
|
}
|
|
|
|
function normalize_spacing(s) {
|
|
if (is_number(s)) return {l:s, r:s, t:s, b:s}
|
|
if (is_array(s)) {
|
|
if (length(s) == 2) return {l:s[0], r:s[0], t:s[1], b:s[1]}
|
|
if (length(s) == 4) return {l:s[0], r:s[1], t:s[2], b:s[3]}
|
|
}
|
|
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}
|
|
}
|
|
|
|
// Tree building state
|
|
var root_item = null
|
|
var tree_root = null
|
|
var config_stack = []
|
|
|
|
// Rewriting state management for cleaner recursion
|
|
var tree_stack = []
|
|
var _next_id = 0
|
|
|
|
function annotate_tree(node, root_height, parent_node) {
|
|
var rect = lay_ctx.get_rect(node.id)
|
|
node.boundingbox = {
|
|
x: rect.x,
|
|
y: root_height - (rect.y + rect.height),
|
|
width: rect.width,
|
|
height: rect.height
|
|
}
|
|
node[CHILDREN] = node.children
|
|
node[PARENT] = parent_node
|
|
arrfor(node.children, function(child) {
|
|
annotate_tree(child, root_height, node)
|
|
})
|
|
}
|
|
|
|
clay.layout = function(fn, size) {
|
|
lay_ctx.reset()
|
|
_next_id = 0
|
|
var root_id = lay_ctx.item()
|
|
|
|
lay_ctx.set_size(root_id, size)
|
|
lay_ctx.set_contain(root_id, layout.contain.row)
|
|
|
|
var root_node = {
|
|
id: root_id,
|
|
config: meme(base_config, {size: size}),
|
|
children: []
|
|
}
|
|
|
|
tree_stack = [root_node]
|
|
|
|
fn() // User builds tree
|
|
|
|
lay_ctx.run()
|
|
|
|
// Annotate tree for clay_input (boundingbox, CHILDREN, PARENT)
|
|
annotate_tree(root_node, size.height, null)
|
|
|
|
// Build flat drawable list and attach to tree root
|
|
root_node.drawables = build_drawables(root_node, size.height)
|
|
return root_node
|
|
}
|
|
|
|
function build_drawables(node, root_height, parent, parent_scissor) {
|
|
var p_abs_x = (parent && parent.x) || 0
|
|
var p_abs_y = (parent && parent.y) || 0
|
|
var p_layer = (parent && parent.layer) || 0
|
|
|
|
var rect = lay_ctx.get_rect(node.id)
|
|
|
|
// Calculate absolute world Y for this node (bottom-up layout to top-down render)
|
|
var abs_y = root_height - (rect.y + rect.height)
|
|
var abs_x = rect.x
|
|
|
|
// IMPORTANT: The offset in config is applied VISUALLY.
|
|
var vis_x = abs_x + node.config.offset.x
|
|
var vis_y = abs_y + node.config.offset.y
|
|
|
|
var drawables = []
|
|
|
|
// Scissor
|
|
var current_scissor = parent_scissor
|
|
var sx = vis_x
|
|
var sy = vis_y
|
|
var sw = rect.width
|
|
var sh = rect.height
|
|
var clip_right = null
|
|
var clip_bottom = null
|
|
if (node.config.clipped) {
|
|
// Intersect with parent
|
|
if (parent_scissor) {
|
|
sx = max(sx, parent_scissor.x)
|
|
sy = max(sy, parent_scissor.y)
|
|
clip_right = min(vis_x + sw, parent_scissor.x + parent_scissor.width)
|
|
clip_bottom = min(vis_y + sh, parent_scissor.y + parent_scissor.height)
|
|
sw = max(0, clip_right - sx)
|
|
sh = max(0, clip_bottom - sy)
|
|
}
|
|
|
|
current_scissor = {x: sx, y: sy, width: sw, height: sh}
|
|
}
|
|
|
|
// Background
|
|
if (node.config.background_image) {
|
|
_next_id = _next_id + 1
|
|
if (node.config.slice) {
|
|
drawables[] = {
|
|
_id: _next_id,
|
|
type: 'sprite',
|
|
image: node.config.background_image,
|
|
pos: {x: vis_x, y: vis_y},
|
|
width: rect.width,
|
|
height: rect.height,
|
|
slice: node.config.slice,
|
|
color: node.config.background_color || {r:1, g:1, b:1, a:1},
|
|
layer: p_layer - 0.1,
|
|
scissor: current_scissor
|
|
}
|
|
} else {
|
|
drawables[] = {
|
|
_id: _next_id,
|
|
type: 'sprite',
|
|
image: node.config.background_image,
|
|
pos: {x: vis_x, y: vis_y},
|
|
width: rect.width,
|
|
height: rect.height,
|
|
color: node.config.background_color || {r:1, g:1, b:1, a:1},
|
|
layer: p_layer - 0.1,
|
|
scissor: current_scissor
|
|
}
|
|
}
|
|
} else if (node.config.background_color) {
|
|
_next_id = _next_id + 1
|
|
drawables[] = {
|
|
_id: _next_id,
|
|
type: 'rect',
|
|
pos: {x: vis_x, y: vis_y},
|
|
width: rect.width,
|
|
height: rect.height,
|
|
color: node.config.background_color,
|
|
layer: p_layer - 0.1,
|
|
scissor: current_scissor
|
|
}
|
|
}
|
|
|
|
// Content (Image/Text)
|
|
if (node.config.image) {
|
|
_next_id = _next_id + 1
|
|
drawables[] = {
|
|
_id: _next_id,
|
|
type: 'sprite',
|
|
image: node.config.image,
|
|
pos: {x: vis_x, y: vis_y},
|
|
width: rect.width,
|
|
height: rect.height,
|
|
color: node.config.color,
|
|
layer: p_layer,
|
|
scissor: current_scissor
|
|
}
|
|
}
|
|
|
|
if (node.config.text) {
|
|
_next_id = _next_id + 1
|
|
drawables[] = {
|
|
_id: _next_id,
|
|
type: 'text',
|
|
text: node.config.text,
|
|
font: node.config.font_path,
|
|
size: node.config.font_size,
|
|
color: node.config.color,
|
|
pos: {x: vis_x, y: vis_y + rect.height},
|
|
anchor_y: 1.0,
|
|
layer: p_layer,
|
|
scissor: current_scissor
|
|
}
|
|
}
|
|
|
|
// Children
|
|
arrfor(node.children, function(child) {
|
|
drawables = array(drawables, build_drawables(child, root_height, {x: vis_x, y: vis_y, layer: p_layer + 0.01}, current_scissor))
|
|
})
|
|
|
|
return drawables
|
|
}
|
|
|
|
|
|
// --- Item Creation Helpers ---
|
|
|
|
function process_configs(configs) {
|
|
var cfg = meme(base_config, configs)
|
|
|
|
// Parse shorthand font string (e.g. 'blackcastle.64') into font_path and font_size
|
|
var font_parts = null
|
|
var parsed_size = null
|
|
if (cfg.font && is_text(cfg.font)) {
|
|
font_parts = array(cfg.font, '.')
|
|
if (length(font_parts) >= 2) {
|
|
parsed_size = number(font_parts[length(font_parts) - 1])
|
|
if (parsed_size) {
|
|
cfg.font_size = parsed_size
|
|
cfg.font_path = font_parts[0]
|
|
}
|
|
}
|
|
}
|
|
|
|
cfg.color = normalize_color(cfg.color, base_config.color)
|
|
if (cfg.background_color) cfg.background_color = normalize_color(cfg.background_color, {r:1,g:1,b:1,a:1})
|
|
|
|
if (!cfg.offset) cfg.offset = {x:0, y:0}
|
|
else cfg.offset = {x: cfg.offset.x || 0, y: cfg.offset.y || 0}
|
|
|
|
return cfg
|
|
}
|
|
|
|
function push_node(configs, contain_mode) {
|
|
var config = process_configs(configs)
|
|
if (contain_mode != null) config.contain = contain_mode
|
|
|
|
var item = lay_ctx.item()
|
|
|
|
// Apply layout props
|
|
lay_ctx.set_margins(item, normalize_spacing(config.margin))
|
|
lay_ctx.set_contain(item, config.contain)
|
|
lay_ctx.set_behave(item, config.behave)
|
|
|
|
var s = config.size
|
|
if (s) {
|
|
if (is_array(s)) s = {width: s[0], height: s[1]}
|
|
lay_ctx.set_size(item, s)
|
|
}
|
|
|
|
var node = {
|
|
id: item,
|
|
config: config,
|
|
children: []
|
|
}
|
|
|
|
// Add to parent
|
|
var parent = tree_stack[length(tree_stack)-1]
|
|
parent.children[] = node
|
|
lay_ctx.insert(parent.id, item)
|
|
|
|
tree_stack[] = node
|
|
return node
|
|
}
|
|
|
|
function pop_node() {
|
|
tree_stack[]
|
|
}
|
|
|
|
// Generic container
|
|
clay.container = function(configs, fn) {
|
|
var _fn = is_function(configs) ? configs : fn
|
|
var _configs = is_function(configs) ? [{}] : (is_array(configs) ? configs : [configs])
|
|
|
|
push_node(_configs, null)
|
|
if (_fn) _fn()
|
|
pop_node()
|
|
}
|
|
|
|
// Stacks
|
|
clay.vstack = function(configs, fn) {
|
|
var _fn = is_function(configs) ? configs : fn
|
|
var _configs = is_function(configs) ? [{}] : (is_array(configs) ? configs : [configs])
|
|
|
|
var c = layout.contain.column
|
|
|
|
push_node(_configs, c)
|
|
if (_fn) _fn()
|
|
pop_node()
|
|
}
|
|
|
|
clay.hstack = function(configs, fn) {
|
|
var _fn = is_function(configs) ? configs : fn
|
|
var _configs = is_function(configs) ? [{}] : (is_array(configs) ? configs : [configs])
|
|
|
|
var c = layout.contain.row
|
|
push_node(_configs, c)
|
|
if (_fn) _fn()
|
|
pop_node()
|
|
}
|
|
|
|
clay.zstack = function(configs, fn) {
|
|
var _fn = is_function(configs) ? configs : fn
|
|
var _configs = is_function(configs) ? [{}] : (is_array(configs) ? configs : [configs])
|
|
|
|
var c = layout.contain.layout
|
|
|
|
push_node(_configs, c)
|
|
if (_fn) _fn()
|
|
pop_node()
|
|
}
|
|
|
|
// Leaf nodes
|
|
clay.image = function(path, configs, extra) {
|
|
var img = graphics.texture(path)
|
|
var c = [{image: path}]
|
|
var _configs = configs ? (is_array(configs) ? configs : [configs]) : []
|
|
if (extra) _configs = array(_configs, is_array(extra) ? extra : [extra])
|
|
var final_config = process_configs(_configs)
|
|
if (!final_config.size && !final_config.behave)
|
|
c[] = {size: {width: img.width, height: img.height}}
|
|
|
|
push_node(array(c, _configs), null)
|
|
pop_node()
|
|
}
|
|
|
|
clay.text = function(str, configs, extra) {
|
|
var c = [{text: str}]
|
|
var _configs = configs ? (is_array(configs) ? configs : [configs]) : []
|
|
if (extra) _configs = array(_configs, is_array(extra) ? extra : [extra])
|
|
var final_config = process_configs(_configs)
|
|
if (!final_config.size && !final_config.behave) {
|
|
c[] = {size: {width: 100, height: 20}}
|
|
}
|
|
|
|
push_node(array(c, _configs), null)
|
|
pop_node()
|
|
}
|
|
|
|
clay.rectangle = function(configs) {
|
|
var _configs = configs ? (is_array(configs) ? configs : [configs]) : [{}]
|
|
push_node(_configs, null)
|
|
pop_node()
|
|
}
|
|
|
|
clay.button = function(str, action, configs) {
|
|
var btn_config = [{
|
|
padding: 10,
|
|
background_color: {r:0.3, g:0.3, b:0.4, a:1}
|
|
}]
|
|
|
|
var _configs = is_array(configs) ? configs : [configs]
|
|
|
|
clay.zstack(array(btn_config, _configs), function() {
|
|
clay.text(str, {color: {r:1,g:1,b:1,a:1}})
|
|
})
|
|
}
|
|
|
|
// Spacer — fills available space
|
|
clay.spacer = function(config) {
|
|
var cfg = config || {}
|
|
if (!cfg.behave) cfg.behave = layout.behave.hfill | layout.behave.vfill
|
|
push_node([cfg], null)
|
|
pop_node()
|
|
}
|
|
|
|
// Convenience draw wrapper — auto-detects call patterns:
|
|
// clay.draw(fn) — default 640x360
|
|
// clay.draw(fn, size_obj) — fn first, size object second
|
|
// clay.draw(size_array, fn) — size array first, fn second
|
|
clay.draw = function(arg1, arg2) {
|
|
var fn = null
|
|
var size = {width: 640, height: 360}
|
|
if (is_function(arg1)) {
|
|
fn = arg1
|
|
if (arg2) {
|
|
if (is_array(arg2)) size = {width: arg2[0], height: arg2[1]}
|
|
else if (is_object(arg2)) size = arg2
|
|
}
|
|
} else if (is_array(arg1)) {
|
|
size = {width: arg1[0], height: arg1[1]}
|
|
fn = arg2
|
|
}
|
|
return clay.layout(fn, size)
|
|
}
|
|
|
|
// Offset all drawables in an array by a position
|
|
clay.offset_drawables = function(drawables, offset) {
|
|
var result = []
|
|
var i = 0
|
|
var d = null
|
|
for (i = 0; i < length(drawables); i = i + 1) {
|
|
d = meme(drawables[i])
|
|
d.pos = {x: d.pos.x + offset.x, y: d.pos.y + offset.y}
|
|
result[] = d
|
|
}
|
|
return result
|
|
}
|
|
|
|
// Constants
|
|
clay.behave = layout.behave
|
|
clay.contain = layout.contain
|
|
|
|
return clay
|