update
This commit is contained in:
35
clay2.cm
35
clay2.cm
@@ -37,6 +37,17 @@ var base_config = {
|
||||
behave: 0
|
||||
}
|
||||
|
||||
function normalize_color(c, fallback) {
|
||||
fallback = fallback || {r:1, g:1, b:1, a:1}
|
||||
if (!c) return {r:fallback.r, g:fallback.g, b:fallback.b, a:fallback.a}
|
||||
return {
|
||||
r: c.r != null ? c.r : fallback.r,
|
||||
g: c.g != null ? c.g : fallback.g,
|
||||
b: c.b != null ? c.b : fallback.b,
|
||||
a: c.a != null ? c.a : fallback.a
|
||||
}
|
||||
}
|
||||
|
||||
function normalize_spacing(s) {
|
||||
if (is_number(s)) return {l:s, r:s, t:s, b:s}
|
||||
if (is_array(s)) {
|
||||
@@ -58,7 +69,6 @@ var tree_stack = []
|
||||
clay.layout = function(fn, size) {
|
||||
lay_ctx.reset()
|
||||
var root_id = lay_ctx.item()
|
||||
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)
|
||||
@@ -90,19 +100,6 @@ function build_drawables(node, root_height, parent_abs_x, parent_abs_y, parent_s
|
||||
var abs_y = root_height - (rect.y + rect.height)
|
||||
var abs_x = rect.x
|
||||
|
||||
// Our absolute position including parent offsets logic from original clay?
|
||||
// Actually layout engine gives rects relative to root usually?
|
||||
// No, layout engine gives RELATIVE logic but `get_rect` usually returns computed layout relative to parent or absolute?
|
||||
// Let's assume `get_rect` returns relative to parent.
|
||||
// Wait, `clay.cm` assumed `rect.x` was absolute?
|
||||
// "Calculate relative position for the group: rel_x = abs_x - parent_abs_x".
|
||||
// This implies `rect.x` is absolute.
|
||||
|
||||
// Let's verify standard behavior. If `layout` returns absolute coords, we don't need to accumulate parent_abs_x for position,
|
||||
// BUT we do need it if `clay` was doing local group offsets.
|
||||
// Original `clay2.cm`: `var rel_x = abs_x - parent_abs_x`. This implies `rect` is absolute.
|
||||
// So `abs_x` IS `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
|
||||
@@ -219,7 +216,15 @@ function build_drawables(node, root_height, parent_abs_x, parent_abs_y, parent_s
|
||||
// --- Item Creation Helpers ---
|
||||
|
||||
function process_configs(configs) {
|
||||
return meme(base_config, ...configs)
|
||||
var cfg = meme(base_config, ...configs)
|
||||
|
||||
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) {
|
||||
|
||||
@@ -3,9 +3,9 @@ function grid(w, h) {
|
||||
this.height = h;
|
||||
// create a height×width array of empty lists
|
||||
this.cells = new Array(h);
|
||||
for (let y = 0; y < h; y++) {
|
||||
for (var y = 0; y < h; y++) {
|
||||
this.cells[y] = new Array(w);
|
||||
for (let x = 0; x < w; x++) {
|
||||
for (var x = 0; x < w; x++) {
|
||||
this.cells[y][x] = []; // each cell holds its own list
|
||||
}
|
||||
}
|
||||
@@ -45,10 +45,10 @@ grid.prototype = {
|
||||
|
||||
// call fn(entity, coord) for every entity in every cell
|
||||
each(fn) {
|
||||
for (let y = 0; y < this.height; y++) {
|
||||
for (let x = 0; x < this.width; x++) {
|
||||
for (var y = 0; y < this.height; y++) {
|
||||
for (var x = 0; x < this.width; x++) {
|
||||
def list = this.cells[y][x];
|
||||
for (let entity of list) {
|
||||
for (var entity of list) {
|
||||
fn(entity, entity.coord);
|
||||
}
|
||||
}
|
||||
@@ -57,9 +57,9 @@ grid.prototype = {
|
||||
|
||||
// printable representation
|
||||
toString() {
|
||||
let out = `grid [${this.width}×${this.height}]\n`;
|
||||
for (let y = 0; y < this.height; y++) {
|
||||
for (let x = 0; x < this.width; x++) {
|
||||
var out = `grid [${this.width}×${this.height}]\n`;
|
||||
for (var y = 0; y < this.height; y++) {
|
||||
for (var x = 0; x < this.width; x++) {
|
||||
out += this.cells[y][x].length;
|
||||
}
|
||||
if (y != this.height - 1) out += "\n";
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
// particles
|
||||
|
||||
return function() {
|
||||
|
||||
}
|
||||
57
playdate.cm
57
playdate.cm
@@ -432,60 +432,3 @@ PlaydateBackend.prototype.cpu_box_blur = function(src, dst, radius) {
|
||||
dst.setData(dst_data)
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// KEY DIFFERENCES FROM SDL3 BACKEND
|
||||
// ========================================================================
|
||||
|
||||
// 1. NO SHADERS
|
||||
// - shader_pass() degrades gracefully (copy, or CPU fallback)
|
||||
// - Complex effects (bloom, CRT) either disabled or approximated
|
||||
|
||||
// 2. NATIVE MASKING
|
||||
// - apply_mask() is BETTER on Playdate (uses setMask() API)
|
||||
// - SDL3 needs shader or stencil buffer
|
||||
// - Playdate has it built-in
|
||||
|
||||
// 3. 1-BIT DISPLAY
|
||||
// - Colors become dither patterns
|
||||
// - Alpha becomes dither density
|
||||
// - Blend modes approximate (XOR for additive)
|
||||
|
||||
// 4. CPU RENDERING
|
||||
// - Some effects run on CPU (blur)
|
||||
// - Slower but possible
|
||||
// - Can skip effects if too slow (check frame time)
|
||||
|
||||
// 5. NO COMMAND BUFFER
|
||||
// - Commands execute immediately
|
||||
// - No batching at GPU level
|
||||
// - But batching still happens at Level 2 (same as SDL3)
|
||||
|
||||
// 6. LIMITED SCALING
|
||||
// - Only nearest-neighbor
|
||||
// - No bilinear filter
|
||||
// - Fine for pixel art!
|
||||
|
||||
// ========================================================================
|
||||
// WHAT STAYS THE SAME
|
||||
// ========================================================================
|
||||
|
||||
// - Scene tree traversal (Level 2)
|
||||
// - Drawable collection (Level 2)
|
||||
// - Sorting (Level 2)
|
||||
// - Batching (Level 2)
|
||||
// - Node graph structure (Level 2)
|
||||
// - High-level API (Level 1)
|
||||
|
||||
// Only THIS FILE changes between SDL3 and Playdate.
|
||||
// Game code is 100% identical.
|
||||
|
||||
// Example: Bullets with emissive lighting
|
||||
//
|
||||
// SDL3: Bullets → bloom shader → additive composite (GPU)
|
||||
// Playdate: Bullets → XOR composite (approximation)
|
||||
//
|
||||
// Same high-level code:
|
||||
// compositor.add_emissive_lighting({tag: 'bullets'})
|
||||
//
|
||||
// Different results, but both correct for their platform.
|
||||
// That's the point of the abstraction!
|
||||
@@ -480,7 +480,6 @@ function pack_model_buffer(material) {
|
||||
return stone(result_blob);
|
||||
}
|
||||
|
||||
var imgui = use('imgui')
|
||||
imgui.init(window, device)
|
||||
|
||||
var rasterize = use('rasterize');
|
||||
|
||||
650
scene.cm
650
scene.cm
@@ -1,650 +0,0 @@
|
||||
// scene.cm - Retained Scene Graph with Dirty Tracking
|
||||
//
|
||||
// Provides retained nodes with:
|
||||
// - Persistent identity and dirty flags
|
||||
// - Cached geometry data
|
||||
// - World transform computation
|
||||
// - Automatic dirty propagation
|
||||
|
||||
var blob_mod = use('blob')
|
||||
|
||||
// Dirty flag bitmask
|
||||
var DIRTY = {
|
||||
NONE: 0,
|
||||
TRANSFORM: 1,
|
||||
CONTENT: 2,
|
||||
CHILDREN: 4,
|
||||
ALL: 7
|
||||
}
|
||||
|
||||
// Base node prototype - all nodes inherit from this
|
||||
var BaseNode = {
|
||||
id: null,
|
||||
type: 'node',
|
||||
dirty: DIRTY.ALL,
|
||||
parent: null,
|
||||
layer: 0,
|
||||
|
||||
// Local properties
|
||||
pos: null,
|
||||
opacity: 1,
|
||||
|
||||
// Computed world properties (updated during scene.update)
|
||||
world_pos: null,
|
||||
world_tint: null,
|
||||
world_opacity: 1,
|
||||
bounds: null,
|
||||
|
||||
// Backend-specific handle (for playdate sprites, etc.)
|
||||
backend_handle: null,
|
||||
|
||||
mark_dirty: function(flags) {
|
||||
this.dirty |= flags
|
||||
},
|
||||
|
||||
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 |= DIRTY.TRANSFORM
|
||||
return this
|
||||
},
|
||||
|
||||
set_opacity: function(o) {
|
||||
if (this.opacity == o) return this
|
||||
this.opacity = o
|
||||
this.dirty |= DIRTY.CONTENT
|
||||
return this
|
||||
}
|
||||
}
|
||||
|
||||
// Sprite node prototype
|
||||
var SpriteNode = {
|
||||
type: 'sprite',
|
||||
|
||||
// Source properties
|
||||
image: null,
|
||||
width: 1,
|
||||
height: 1,
|
||||
anchor: null,
|
||||
color: null,
|
||||
uv_rect: null,
|
||||
slice: null,
|
||||
tile: null,
|
||||
material: null,
|
||||
|
||||
// Geometry cache
|
||||
geom_cache: null,
|
||||
|
||||
set_image: function(img) {
|
||||
if (this.image == img) return this
|
||||
this.image = img
|
||||
this.dirty |= DIRTY.CONTENT
|
||||
if (this.geom_cache) this.geom_cache.texture_key = img
|
||||
return this
|
||||
},
|
||||
|
||||
set_size: function(w, h) {
|
||||
if (this.width == w && this.height == h) return this
|
||||
this.width = w
|
||||
this.height = h
|
||||
this.dirty |= DIRTY.CONTENT
|
||||
return this
|
||||
},
|
||||
|
||||
set_anchor: function(x, y) {
|
||||
if (!this.anchor) this.anchor = {x: 0, y: 0}
|
||||
if (this.anchor.x == x && this.anchor.y == y) return this
|
||||
this.anchor.x = x
|
||||
this.anchor.y = y
|
||||
this.dirty |= DIRTY.TRANSFORM
|
||||
return this
|
||||
},
|
||||
|
||||
set_color: function(r, g, b, a) {
|
||||
if (!this.color) this.color = {r: 1, g: 1, b: 1, a: 1}
|
||||
if (this.color.r == r && this.color.g == g && this.color.b == b && this.color.a == a) return this
|
||||
this.color.r = r
|
||||
this.color.g = g
|
||||
this.color.b = b
|
||||
this.color.a = a
|
||||
this.dirty |= DIRTY.CONTENT
|
||||
return this
|
||||
},
|
||||
|
||||
rebuild_geometry: function() {
|
||||
if (this.slice) return this._build_9slice_geom()
|
||||
if (this.tile) return this._build_tiled_geom()
|
||||
return this._build_quad_geom()
|
||||
},
|
||||
|
||||
_build_quad_geom: function() {
|
||||
if (!this.geom_cache) {
|
||||
this.geom_cache = {
|
||||
verts: null,
|
||||
indices: null,
|
||||
vert_count: 0,
|
||||
index_count: 0,
|
||||
texture_key: this.image
|
||||
}
|
||||
}
|
||||
|
||||
var verts = new blob_mod(128) // 4 verts * 8 floats * 4 bytes
|
||||
var indices = new blob_mod(12) // 6 indices * 2 bytes
|
||||
|
||||
var ax = this.anchor ? this.anchor.x : 0
|
||||
var ay = this.anchor ? this.anchor.y : 0
|
||||
var x = this.world_pos.x - this.width * ax
|
||||
var y = this.world_pos.y - this.height * ay
|
||||
var w = this.width
|
||||
var h = this.height
|
||||
|
||||
var c = this.color || {r: 1, g: 1, b: 1, a: 1}
|
||||
var alpha = c.a * this.world_opacity
|
||||
|
||||
var uv = this.uv_rect || {x: 0, y: 0, width: 1, height: 1}
|
||||
var u0 = uv.x
|
||||
var v0 = uv.y
|
||||
var u1 = uv.x + uv.width
|
||||
var v1 = uv.y + uv.height
|
||||
|
||||
// v0: bottom-left
|
||||
verts.wf(x); verts.wf(y)
|
||||
verts.wf(u0); verts.wf(v1)
|
||||
verts.wf(c.r); verts.wf(c.g); verts.wf(c.b); verts.wf(alpha)
|
||||
|
||||
// v1: bottom-right
|
||||
verts.wf(x + w); verts.wf(y)
|
||||
verts.wf(u1); verts.wf(v1)
|
||||
verts.wf(c.r); verts.wf(c.g); verts.wf(c.b); verts.wf(alpha)
|
||||
|
||||
// v2: top-right
|
||||
verts.wf(x + w); verts.wf(y + h)
|
||||
verts.wf(u1); verts.wf(v0)
|
||||
verts.wf(c.r); verts.wf(c.g); verts.wf(c.b); verts.wf(alpha)
|
||||
|
||||
// v3: top-left
|
||||
verts.wf(x); verts.wf(y + h)
|
||||
verts.wf(u0); verts.wf(v0)
|
||||
verts.wf(c.r); verts.wf(c.g); verts.wf(c.b); verts.wf(alpha)
|
||||
|
||||
indices.w16(0); indices.w16(1); indices.w16(2)
|
||||
indices.w16(0); indices.w16(2); indices.w16(3)
|
||||
|
||||
this.geom_cache.verts = stone(verts)
|
||||
this.geom_cache.indices = stone(indices)
|
||||
this.geom_cache.vert_count = 4
|
||||
this.geom_cache.index_count = 6
|
||||
this.geom_cache.texture_key = this.image
|
||||
},
|
||||
|
||||
_build_9slice_geom: function() {
|
||||
// TODO: Implement 9-slice geometry building
|
||||
this._build_quad_geom()
|
||||
},
|
||||
|
||||
_build_tiled_geom: function() {
|
||||
// TODO: Implement tiled geometry building
|
||||
this._build_quad_geom()
|
||||
}
|
||||
}
|
||||
|
||||
// Tilemap node prototype
|
||||
var TilemapNode = {
|
||||
type: 'tilemap',
|
||||
|
||||
tile_size: null,
|
||||
tiles: null,
|
||||
offset: null,
|
||||
|
||||
geom_cache: null,
|
||||
|
||||
set_tile: function(x, y, image) {
|
||||
if (!this.tiles) this.tiles = []
|
||||
if (!this.tiles[x]) this.tiles[x] = []
|
||||
if (this.tiles[x][y] == image) return this
|
||||
this.tiles[x][y] = image
|
||||
this.dirty |= DIRTY.CONTENT
|
||||
return this
|
||||
},
|
||||
|
||||
rebuild_geometry: function() {
|
||||
if (!this.geom_cache) {
|
||||
this.geom_cache = {
|
||||
verts: null,
|
||||
indices: null,
|
||||
vert_count: 0,
|
||||
index_count: 0,
|
||||
batches: []
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.tiles) {
|
||||
this.geom_cache.vert_count = 0
|
||||
this.geom_cache.index_count = 0
|
||||
this.geom_cache.batches = []
|
||||
return
|
||||
}
|
||||
|
||||
// Count tiles and group by texture
|
||||
var tile_count = 0
|
||||
var texture_map = {}
|
||||
|
||||
var ts = this.tile_size || {x: 1, y: 1}
|
||||
var off = this.offset || {x: 0, y: 0}
|
||||
|
||||
for (var x = 0; x < this.tiles.length; x++) {
|
||||
if (!this.tiles[x]) continue
|
||||
for (var y = 0; y < this.tiles[x].length; y++) {
|
||||
var t = this.tiles[x][y]
|
||||
if (!t) continue
|
||||
if (!texture_map[t]) texture_map[t] = []
|
||||
texture_map[t].push({x: x, y: y})
|
||||
tile_count++
|
||||
}
|
||||
}
|
||||
|
||||
if (tile_count == 0) {
|
||||
this.geom_cache.vert_count = 0
|
||||
this.geom_cache.index_count = 0
|
||||
this.geom_cache.batches = []
|
||||
return
|
||||
}
|
||||
|
||||
var verts = new blob_mod(tile_count * 4 * 32)
|
||||
var indices = new blob_mod(tile_count * 6 * 2)
|
||||
|
||||
var vert_offset = 0
|
||||
var index_offset = 0
|
||||
var batches = []
|
||||
|
||||
for (var tex in texture_map) {
|
||||
var batch_start = index_offset / 2
|
||||
var tiles_list = texture_map[tex]
|
||||
|
||||
for (var i = 0; i < tiles_list.length; i++) {
|
||||
var tile = tiles_list[i]
|
||||
var wx = this.world_pos.x + (tile.x + off.x) * ts.x
|
||||
var wy = this.world_pos.y + (tile.y + off.y) * ts.y
|
||||
var tw = ts.x
|
||||
var th = ts.y
|
||||
|
||||
// 4 vertices
|
||||
verts.wf(wx); verts.wf(wy); verts.wf(0); verts.wf(1)
|
||||
verts.wf(1); verts.wf(1); verts.wf(1); verts.wf(this.world_opacity)
|
||||
|
||||
verts.wf(wx + tw); verts.wf(wy); verts.wf(1); verts.wf(1)
|
||||
verts.wf(1); verts.wf(1); verts.wf(1); verts.wf(this.world_opacity)
|
||||
|
||||
verts.wf(wx + tw); verts.wf(wy + th); verts.wf(1); verts.wf(0)
|
||||
verts.wf(1); verts.wf(1); verts.wf(1); verts.wf(this.world_opacity)
|
||||
|
||||
verts.wf(wx); verts.wf(wy + th); verts.wf(0); verts.wf(0)
|
||||
verts.wf(1); verts.wf(1); verts.wf(1); verts.wf(this.world_opacity)
|
||||
|
||||
// 6 indices
|
||||
var base = vert_offset
|
||||
indices.w16(base); indices.w16(base + 1); indices.w16(base + 2)
|
||||
indices.w16(base); indices.w16(base + 2); indices.w16(base + 3)
|
||||
|
||||
vert_offset += 4
|
||||
index_offset += 12
|
||||
}
|
||||
|
||||
batches.push({
|
||||
texture: tex,
|
||||
start_index: batch_start,
|
||||
index_count: tiles_list.length * 6
|
||||
})
|
||||
}
|
||||
|
||||
this.geom_cache.verts = stone(verts)
|
||||
this.geom_cache.indices = stone(indices)
|
||||
this.geom_cache.vert_count = vert_offset
|
||||
this.geom_cache.index_count = index_offset / 2
|
||||
this.geom_cache.batches = batches
|
||||
}
|
||||
}
|
||||
|
||||
// Text node prototype
|
||||
var TextNode = {
|
||||
type: 'text',
|
||||
|
||||
text: '',
|
||||
font: 'fonts/dos',
|
||||
size: 16,
|
||||
mode: 'bitmap',
|
||||
color: null,
|
||||
anchor: null,
|
||||
outline_width: 0,
|
||||
outline_color: null,
|
||||
|
||||
measured: null,
|
||||
geom_cache: null,
|
||||
|
||||
set_text: function(t) {
|
||||
if (this.text == t) return this
|
||||
this.text = t
|
||||
this.dirty |= DIRTY.CONTENT
|
||||
return this
|
||||
},
|
||||
|
||||
set_font: function(f, s, m) {
|
||||
if (this.font == f && this.size == s && this.mode == m) return this
|
||||
this.font = f
|
||||
if (s != null) this.size = s
|
||||
if (m != null) this.mode = m
|
||||
this.dirty |= DIRTY.CONTENT
|
||||
return this
|
||||
},
|
||||
|
||||
rebuild_geometry: function(font_system) {
|
||||
// Text geometry is built by the backend using font_system
|
||||
// We just mark that we need rebuild
|
||||
if (!this.geom_cache) {
|
||||
this.geom_cache = {
|
||||
verts: null,
|
||||
indices: null,
|
||||
vert_count: 0,
|
||||
index_count: 0,
|
||||
font_texture: null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Group node prototype
|
||||
var GroupNode = {
|
||||
type: 'group',
|
||||
|
||||
children: null,
|
||||
effects: null,
|
||||
space: '2d',
|
||||
scissor: null,
|
||||
|
||||
bounds: null,
|
||||
|
||||
add_child: function(node) {
|
||||
if (!this.children) this.children = []
|
||||
node.parent = this
|
||||
this.children.push(node)
|
||||
this.dirty |= DIRTY.CHILDREN
|
||||
return this
|
||||
},
|
||||
|
||||
remove_child: function(node) {
|
||||
if (!this.children) return this
|
||||
var idx = this.children.indexOf(node)
|
||||
if (idx >= 0) {
|
||||
this.children.splice(idx, 1)
|
||||
node.parent = null
|
||||
this.dirty |= DIRTY.CHILDREN
|
||||
}
|
||||
return this
|
||||
},
|
||||
|
||||
clear_children: function() {
|
||||
if (!this.children) return this
|
||||
for (var c of this.children) {
|
||||
c.parent = null
|
||||
}
|
||||
this.children = []
|
||||
this.dirty |= DIRTY.CHILDREN
|
||||
return this
|
||||
}
|
||||
}
|
||||
|
||||
// Particle node prototype
|
||||
var ParticleNode = {
|
||||
type: 'particles',
|
||||
|
||||
image: null,
|
||||
width: 1,
|
||||
height: 1,
|
||||
particles: null,
|
||||
max_particles: 1000,
|
||||
|
||||
geom_cache: null,
|
||||
|
||||
init: function() {
|
||||
if (!this.geom_cache) {
|
||||
this.geom_cache = {
|
||||
verts: null,
|
||||
indices: null,
|
||||
active_count: 0,
|
||||
capacity: this.max_particles
|
||||
}
|
||||
}
|
||||
|
||||
// Pre-allocate index buffer (never changes)
|
||||
var indices = new blob_mod(this.max_particles * 6 * 2)
|
||||
for (var i = 0; i < this.max_particles; i++) {
|
||||
var base = i * 4
|
||||
indices.w16(base)
|
||||
indices.w16(base + 1)
|
||||
indices.w16(base + 2)
|
||||
indices.w16(base)
|
||||
indices.w16(base + 2)
|
||||
indices.w16(base + 3)
|
||||
}
|
||||
this.geom_cache.indices = stone(indices)
|
||||
return this
|
||||
},
|
||||
|
||||
rebuild_geometry: function() {
|
||||
if (!this.particles || this.particles.length == 0) {
|
||||
if (this.geom_cache) this.geom_cache.active_count = 0
|
||||
return
|
||||
}
|
||||
|
||||
if (!this.geom_cache) this.init()
|
||||
|
||||
var verts = new blob_mod(this.max_particles * 4 * 32)
|
||||
var count = 0
|
||||
|
||||
for (var i = 0; i < this.particles.length && count < this.max_particles; i++) {
|
||||
var p = this.particles[i]
|
||||
var hw = this.width * (p.scale || 1) * 0.5
|
||||
var hh = this.height * (p.scale || 1) * 0.5
|
||||
var px = this.world_pos.x + (p.pos ? p.pos.x : 0)
|
||||
var py = this.world_pos.y + (p.pos ? p.pos.y : 0)
|
||||
var c = p.color || {r: 1, g: 1, b: 1, a: 1}
|
||||
|
||||
// v0: bottom-left
|
||||
verts.wf(px - hw); verts.wf(py - hh); verts.wf(0); verts.wf(1)
|
||||
verts.wf(c.r); verts.wf(c.g); verts.wf(c.b); verts.wf(c.a)
|
||||
|
||||
// v1: bottom-right
|
||||
verts.wf(px + hw); verts.wf(py - hh); verts.wf(1); verts.wf(1)
|
||||
verts.wf(c.r); verts.wf(c.g); verts.wf(c.b); verts.wf(c.a)
|
||||
|
||||
// v2: top-right
|
||||
verts.wf(px + hw); verts.wf(py + hh); verts.wf(1); verts.wf(0)
|
||||
verts.wf(c.r); verts.wf(c.g); verts.wf(c.b); verts.wf(c.a)
|
||||
|
||||
// v3: top-left
|
||||
verts.wf(px - hw); verts.wf(py + hh); verts.wf(0); verts.wf(0)
|
||||
verts.wf(c.r); verts.wf(c.g); verts.wf(c.b); verts.wf(c.a)
|
||||
|
||||
count++
|
||||
}
|
||||
|
||||
this.geom_cache.verts = stone(verts)
|
||||
this.geom_cache.active_count = count
|
||||
}
|
||||
}
|
||||
|
||||
// Scene graph manager
|
||||
var SceneGraph = {
|
||||
root: null,
|
||||
all_nodes: null,
|
||||
dirty_nodes: null,
|
||||
_id_counter: 0,
|
||||
|
||||
stats: {
|
||||
total_nodes: 0,
|
||||
dirty_this_frame: 0,
|
||||
geometry_rebuilds: 0
|
||||
},
|
||||
|
||||
_gen_id: function() {
|
||||
return 'node_' + text(this._id_counter++)
|
||||
},
|
||||
|
||||
create: function(type, props) {
|
||||
props = props || {}
|
||||
var node = null
|
||||
|
||||
switch (type) {
|
||||
case 'sprite':
|
||||
node = meme(SpriteNode)
|
||||
break
|
||||
case 'tilemap':
|
||||
node = meme(TilemapNode)
|
||||
break
|
||||
case 'text':
|
||||
node = meme(TextNode)
|
||||
break
|
||||
case 'group':
|
||||
node = meme(GroupNode)
|
||||
break
|
||||
case 'particles':
|
||||
node = meme(ParticleNode)
|
||||
break
|
||||
default:
|
||||
node = meme(BaseNode)
|
||||
node.type = type || 'node'
|
||||
}
|
||||
|
||||
// Apply base node properties
|
||||
node.id = props.id || this._gen_id()
|
||||
node.dirty = DIRTY.ALL
|
||||
node.parent = null
|
||||
node.layer = props.layer || 0
|
||||
node.pos = props.pos || {x: 0, y: 0}
|
||||
node.opacity = props.opacity != null ? props.opacity : 1
|
||||
node.world_pos = {x: 0, y: 0}
|
||||
node.world_tint = {r: 1, g: 1, b: 1, a: 1}
|
||||
node.world_opacity = 1
|
||||
|
||||
// Apply type-specific properties
|
||||
for (var k in props) {
|
||||
if (k != 'id' && k != 'layer' && k != 'pos' && k != 'opacity') {
|
||||
node[k] = props[k]
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.all_nodes) this.all_nodes = {}
|
||||
this.all_nodes[node.id] = node
|
||||
this.stats.total_nodes++
|
||||
|
||||
return node
|
||||
},
|
||||
|
||||
remove: function(node) {
|
||||
if (!node) return
|
||||
if (this.all_nodes && this.all_nodes[node.id]) {
|
||||
delete this.all_nodes[node.id]
|
||||
this.stats.total_nodes--
|
||||
}
|
||||
if (node.parent) {
|
||||
node.parent.remove_child(node)
|
||||
}
|
||||
// Recursively remove children
|
||||
if (node.children) {
|
||||
for (var c of node.children) {
|
||||
this.remove(c)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Update world transforms and rebuild dirty geometry
|
||||
update: function(font_system) {
|
||||
this.stats.dirty_this_frame = 0
|
||||
this.stats.geometry_rebuilds = 0
|
||||
this.dirty_nodes = []
|
||||
|
||||
if (!this.root) return
|
||||
|
||||
// Phase 1: Propagate transforms
|
||||
this._update_transforms(this.root, {x: 0, y: 0}, {r: 1, g: 1, b: 1, a: 1}, 1)
|
||||
|
||||
// Phase 2: Rebuild geometry for dirty nodes
|
||||
for (var i = 0; i < this.dirty_nodes.length; i++) {
|
||||
var node = this.dirty_nodes[i]
|
||||
if (node.rebuild_geometry) {
|
||||
if (node.type == 'text') {
|
||||
node.rebuild_geometry(font_system)
|
||||
} else {
|
||||
node.rebuild_geometry()
|
||||
}
|
||||
this.stats.geometry_rebuilds++
|
||||
}
|
||||
node.dirty = DIRTY.NONE
|
||||
}
|
||||
},
|
||||
|
||||
_update_transforms: function(node, parent_pos, parent_tint, parent_opacity) {
|
||||
if (!node) return
|
||||
|
||||
var pos_changed = (node.dirty & DIRTY.TRANSFORM) != 0
|
||||
var content_changed = (node.dirty & DIRTY.CONTENT) != 0
|
||||
|
||||
// Compute world position
|
||||
var node_pos = node.pos || {x: 0, y: 0}
|
||||
node.world_pos = {x: parent_pos.x + node_pos.x, y: parent_pos.y + node_pos.y}
|
||||
|
||||
// Compute world tint
|
||||
var nt = node.color || node.tint || {r: 1, g: 1, b: 1, a: 1}
|
||||
node.world_tint = {
|
||||
r: parent_tint.r * nt.r,
|
||||
g: parent_tint.g * nt.g,
|
||||
b: parent_tint.b * nt.b,
|
||||
a: parent_tint.a * nt.a
|
||||
}
|
||||
|
||||
node.world_opacity = parent_opacity * (node.opacity != null ? node.opacity : 1)
|
||||
|
||||
// Mark for geometry rebuild if needed
|
||||
if (pos_changed || content_changed) {
|
||||
this.dirty_nodes.push(node)
|
||||
this.stats.dirty_this_frame++
|
||||
}
|
||||
|
||||
// Recurse children
|
||||
if (node.children) {
|
||||
for (var i = 0; i < node.children.length; i++) {
|
||||
this._update_transforms(node.children[i], node.world_pos, node.world_tint, node.world_opacity)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Find node by id
|
||||
find: function(id) {
|
||||
return this.all_nodes ? this.all_nodes[id] : null
|
||||
},
|
||||
|
||||
// Clear entire scene
|
||||
clear: function() {
|
||||
this.root = null
|
||||
this.all_nodes = {}
|
||||
this.dirty_nodes = []
|
||||
this._id_counter = 0
|
||||
this.stats.total_nodes = 0
|
||||
}
|
||||
}
|
||||
|
||||
// Factory function - returns a new scene graph instance
|
||||
return function() {
|
||||
var sg = meme(SceneGraph)
|
||||
sg.all_nodes = {}
|
||||
sg.dirty_nodes = []
|
||||
sg._id_counter = 0
|
||||
sg.stats = {
|
||||
total_nodes: 0,
|
||||
dirty_this_frame: 0,
|
||||
geometry_rebuilds: 0
|
||||
}
|
||||
return sg
|
||||
}
|
||||
@@ -73,7 +73,7 @@ var fpsTimer=0, fpsCount=0
|
||||
|
||||
Anim.minDelay = 1 / 100; // 10 ms, feel free to tune later
|
||||
|
||||
let last = os.now();
|
||||
var last = os.now();
|
||||
|
||||
function loop(){
|
||||
def now = os.now();
|
||||
|
||||
@@ -6,7 +6,7 @@ var json = use('json');
|
||||
var cameras = camera.list();
|
||||
if (cameras.length == 0) {
|
||||
log.console("No cameras found!");
|
||||
$ stop();
|
||||
$stop();
|
||||
}
|
||||
|
||||
var cam_id = cameras[0];
|
||||
|
||||
Reference in New Issue
Block a user