commit da2fe9667a3110b4b52f5d10bdfe30575a436720 Author: John Alanbrook Date: Sat Nov 22 09:43:51 2025 -0600 initial add diff --git a/camera.cm b/camera.cm new file mode 100644 index 00000000..868f3da0 --- /dev/null +++ b/camera.cm @@ -0,0 +1,121 @@ +var cam = {} + +/* + presentation can be one of + letterbox + overscan + stretch + ... or simply 'null' for no presentation +*/ + +var basecam = { + pos: [0,0], // where it is + ortho:true, + width: 100, + aspect_ratio: 16/9, + fov:50, + near_z:0, + far_z:1000, + anchor:[0.5,0.5], + rotation:[0,0,0,1], + presentation: "letterbox", + background: {r:1,g:1,b:1,a:0}, + viewport: {x:0,y:0,width:1,height:1}, +} + +basecam.draw_rect = function(size) +{ + var mode = this.presentation || "letterbox" + var vp = { + x:this.viewport.x, + y:1-this.viewport.y-this.viewport.height, + width:this.viewport.width, + height:this.viewport.height + } + var src_rect = {x:0,y:0,width:this.size.x,height:this.size.y} + var dst_rect = {x:vp.x*size.x,y:vp.y*size.y,width:vp.width*size.x,height:vp.height*size.y}; + return mode_rect(src_rect,dst_rect,mode); +} + +basecam.screen2camera = function(pos) +{ + var draw_rect = this.draw_rect(prosperon.window.size); + var ret = [pos.x-draw_rect.x, pos.y - draw_rect.y]; + ret.x /= draw_rect.width; + ret.y /= draw_rect.height; + ret.y = 1 - ret.y; + return ret; +} + +basecam.screen2hud = function(pos) +{ + var cam = this.screen2camera(pos); + cam.x *= this.size.x; + cam.y *= this.size.y; + return cam; +} + +basecam.screen2world = function(pos) +{ + var hud = this.screen2hud(pos); + hud.x += this.transform.pos.x - this.size.x/2; + hud.y += this.transform.pos.y - this.size.y/2; + return hud; +} + +function mode_rect(src,dst,mode = "stretch") +{ + var aspect_src = src.width/src.height; + var aspect_dst = dst.width/dst.height; + var out = { + x:dst.x, + y:dst.y, + width:dst.width, + height:dst.height + }; + if (mode == "stretch") return out; + + if (mode == "letterbox") { + if (aspect_src > aspect_dst) { + var scaled_h = out.width/aspect_src; + var off = (out.height - scaled_h) * 0.5; + out.y += off; + out.height = scaled_h; + } else { + var scaled_w =out.height * aspect_src; + var off = (out.width - scaled_w) * 0.5; + out.x += off; + out.width = scaled_w; + } + } else if (mode == "overscan"){ + if (aspect_src > aspect_dst) { + var scaled_w = out.height * aspect_src; + var off = (out.width - scaled_w) * 0.5; + out.x += off; + out.width = scaled_w; + } else { + var scaled_h = out.width / aspect_src; + var off = (out.height - scaled_h) * 0.5; + out.y += off; + out.height = scaled_h; + } + } + return out; +} + +cam.make = function() +{ + var c = Object.create(basecam) + c.transform = new transform + c.transform.unit() + c.size = [640,360] + c.mode = 'keep' + c.viewport = {x:0,y:0,width:1,height:1} + c.fov = 45 + c.type = 'ortho' + c.ortho = true + c.aspect = 16/9 + return c +} + +return cam diff --git a/clay.cm b/clay.cm new file mode 100644 index 00000000..ea21d155 --- /dev/null +++ b/clay.cm @@ -0,0 +1,410 @@ +// Layout code +// Contain is for how it will treat its children. If they should be laid out as a row, or column, or in a flex style, etc. + +var layout = use('layout') +var geometry = use('geometry') +var draw = use('prosperon/draw2d') +var graphics = use('graphics') +var util = use('util') +var input = use('input') +var prosperon = use('prosperon') + +var CHILDREN = Symbol('children') +var PARENT = Symbol('parent') + +function normalizeSpacing(spacing) { + if (typeof spacing == 'number') { + return {l: spacing, r: spacing, t: spacing, b: spacing} + } else if (Array.isArray(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') { + 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} + } +} + +var lay_ctx = layout.make_context(); + +var clay_base = { + font: null, + background_image: null, + slice: 0, + font: 'smalle.16', + font_size: null, + 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, // {width: null, height: null} +}; + +var root_item; +var root_config; +var tree_root; +var clay = {} +clay.CHILDREN = CHILDREN +clay.PARENT = PARENT + +var focused_textbox = null + +clay.behave = layout.behave; +clay.contain = layout.contain; + +clay.draw = function draw(fn, size = [prosperon.camera.width, prosperon.camera.height]) +{ + lay_ctx.reset(); + var root = lay_ctx.item(); + // Accept both array and object formats + if (Array.isArray(size)) { + size = {width: size[0], height: size[1]}; + } + lay_ctx.set_size(root,size); + lay_ctx.set_contain(root,layout.contain.row); + root_item = root; + root_config = Object.assign({}, clay_base); + tree_root = { + id: root, + config: root_config, + }; + tree_root[CHILDREN] = []; + tree_root[PARENT] = null; + fn() + lay_ctx.run(); + + // Adjust bounding boxes for padding - traverse tree instead of array + function adjust_bounding_boxes(node) { + node.content = lay_ctx.get_rect(node.id); + node.boundingbox = Object.assign({}, node.content); + + var padding = normalizeSpacing(node.config.padding || 0); + + node.boundingbox.x -= padding.l; + node.boundingbox.y -= padding.t; + node.boundingbox.width += padding.l + padding.r; + node.boundingbox.height += padding.t + padding.b; + + node.marginbox = Object.assign({}, node.content); + var margin = normalizeSpacing(node.config.margin || 0); + node.marginbox.x -= margin.l; + node.marginbox.y -= margin.t; + node.marginbox.width += margin.l+margin.r; + node.marginbox.height += margin.t+margin.b; + + // Apply max_size clamping post-layout + if (node.config.max_size) { + if (node.config.max_size.width != null) { + // Clamp the layout rect size + var rect = lay_ctx.get_rect(node.id); + rect.width = Math.min(rect.width, node.config.max_size.width); + // Also clamp bounding box + node.content.width = Math.min(node.content.width, node.config.max_size.width); + node.boundingbox.width = Math.min(node.boundingbox.width, node.config.max_size.width + padding.l + padding.r); + node.marginbox.width = Math.min(node.marginbox.width, node.config.max_size.width + padding.l + padding.r + margin.l + margin.r); + } + if (node.config.max_size.height != null) { + // Clamp the layout rect size + var rect = lay_ctx.get_rect(node.id); + rect.height = Math.min(rect.height, node.config.max_size.height); + // Also clamp bounding box + node.content.height = Math.min(node.content.height, node.config.max_size.height); + node.boundingbox.height = Math.min(node.boundingbox.height, node.config.max_size.height + padding.t + padding.b); + node.marginbox.height = Math.min(node.marginbox.height, node.config.max_size.height + padding.t + padding.b + margin.t + margin.b); + } + } + + node.content.y *= -1; + node.content.y += size.height; + node.boundingbox.y *= -1; + node.boundingbox.y += size.height; + node.content.anchor_y = 1; + node.boundingbox.anchor_y = 1; + + // Recursively adjust children + if (node[CHILDREN]) { + for (var child of node[CHILDREN]) { + adjust_bounding_boxes(child); + } + } + } + + adjust_bounding_boxes(tree_root); + + return tree_root; +} + +function create_view_fn(base_config) +{ + var base = Object.assign(Object.create(clay_base), base_config); + return function view(config = {}, fn) { + config.__proto__ = base; + var item = add_item(config); + + var prev_item = root_item; + var prev_config = root_config; + var prev_tree_root = tree_root; + root_item = item; + root_config = config; + root_config._childIndex = 0; // Initialize child index + // Find the tree node for this item and set it as current tree_root + if (prev_tree_root[CHILDREN] && prev_tree_root[CHILDREN].length > 0) { + tree_root = prev_tree_root[CHILDREN][prev_tree_root[CHILDREN].length - 1]; + } else { + // If no children yet, this shouldn't happen, but handle gracefully + tree_root = prev_tree_root; + } + fn?.(); + root_item = prev_item; + root_config = prev_config; + tree_root = prev_tree_root; + } +} + +clay.vstack = create_view_fn({ + contain: layout.contain.column | layout.contain.start, +}); + +clay.hstack = create_view_fn({ + contain: layout.contain.row | layout.contain.start, +}); + +clay.spacer = create_view_fn({ + behave: layout.behave.hfill | layout.behave.vfill +}); + +clay.frame = create_view_fn({}); + +function image_size(img) +{ + return [img.width * (img.rect?.width || 1), img.height * (img.rect?.height || 1)]; +} + +function add_item(config) +{ + // Normalize the child's margin + var margin = normalizeSpacing(config.margin || 0); + var padding = normalizeSpacing(config.padding || 0); + var childGap = root_config.child_gap || 0; + + // Adjust for child_gap + root_config._childIndex ??= 0 + if (root_config._childIndex > 0) { + var parentContain = root_config.contain || 0; + var directionMask = layout.contain.row | layout.contain.column; + var direction = parentContain & directionMask; + var isVStack = direction == layout.contain.column; + var isHStack = direction == layout.contain.row; + + if (isVStack) { + margin.t += childGap; + } else if (isHStack) { + margin.l += childGap; + } + } + + var use_config = Object.create(config); + + use_config.margin = { + t: margin.t+padding.t, + b: margin.b+padding.b, + r:margin.r+padding.r, + l:margin.l+padding.l + }; + + var item = lay_ctx.item(); + lay_ctx.set_margins(item, use_config.margin); + use_config.size ??= {width:0, height:0} + // Convert array to object if needed + if (Array.isArray(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 + if (use_config.max_size) { + // max_size should not force explicit sizing pre-layout - let children compute natural size, + // then clamp the container after layout. For now, just ensure we don't set size to max_size. + } + lay_ctx.set_size(item,use_config.size); + lay_ctx.set_contain(item,use_config.contain); + lay_ctx.set_behave(item,use_config.behave); + var tree_node = { + id: item, + config: use_config, + }; + tree_node[CHILDREN] = []; + tree_node[PARENT] = tree_root; + tree_root[CHILDREN].push(tree_node); + lay_ctx.insert(root_item,item); + + // Increment the parent's child index + root_config._childIndex++; + return item; +} + +function rectify_configs(config_array) +{ + if (config_array.length == 0) + config_array = [{}]; + + for (var i = config_array.length-1; i > 0; i--) + config_array[i].__proto__ = config_array[i-1]; + + config_array[0].__proto__ = clay_base; + var cleanobj = Object.create(config_array[config_array.length-1]); + + return cleanobj; +} + +clay.image = function image(path, ...configs) +{ + var config = rectify_configs(configs); + var image = graphics.texture(path); + config.image = path; // Store the path string, not the texture object + config.size ??= {width: image.width, height: image.height}; + add_item(config); +} + +clay.text = function text(str, ...configs) +{ + var config = rectify_configs(configs); + config.size ??= [0,0] + config.font = graphics.get_font(config.font) + config.text = str + var tsize = config.font.text_size(str, 0, config.size[0], config.text_break, config.text_align); + tsize.x = Math.ceil(tsize.x) + tsize.y = Math.ceil(tsize.y) + config.size = config.size.map((x,i) => Math.max(x, tsize[i])); + add_item(config); +} + +/* + For a given size, + the layout engine should "see" size + margin + but its interior content should "see" size - padding + hence, the layout box should be size-padding, with margin of margin+padding +*/ + +var button_base = Object.assign(Object.create(clay_base), { + padding:0, + hovered:{ + } +}); +clay.button = function button(str, action, config = {}) +{ + config.__proto__ = button_base; + config.font = graphics.get_font(config.font) + config.size = config.font.text_size(str, 0, 0, config.text_break, config.text_align) + add_item(config); + config.text = str; + config.action = action; +} + +var point = use('point') + +clay.draw_commands = function draw_commands(tree_root, pos = {x:0,y:0}) +{ + function draw_node(node) { + var config = node.config + var boundingbox = geometry.rect_move(node.boundingbox,point.add(pos,config.offset)) + var content = geometry.rect_move(node.content,point.add(pos, config.offset)) + + if (config.background_image) + if (config.slice) + draw.slice9(config.background_image, boundingbox, config.slice, config.background_color) + else + draw.image(config.background_image, boundingbox, 0, config.color) + else if (config.background_color) + draw.rectangle(boundingbox, null, {color:config.background_color}) + + if (config.text) { + var baseline_y = content.y + content.height - config.font.ascent + draw.text(config.text, {x: content.x, y: baseline_y}, config.font, config.color, content.width) + } + + if (config.image) + draw.image(config.image, content, 0, config.color) + + if (config.clipped) { + draw.scissor(content) + } + + // Recursively draw children + if (node[CHILDREN]) { + for (var child of node[CHILDREN]) { + draw_node(child); + } + } + if (config.clipped) + draw.scissor(null) + } + + draw_node(tree_root); +} + +var dbg_colors = {}; +clay.debug_colors = dbg_colors; +dbg_colors.content = [1,0,0,0.1]; +dbg_colors.boundingbox = [0,1,0,0,0.1]; +dbg_colors.margin = [0,0,1,0.1]; +clay.draw_debug = function draw_debug(tree_root, pos = {x:0, y:0}) +{ + function draw_debug_node(node) { + var boundingbox = geometry.rect_move(node.boundingbox,pos); + var content = geometry.rect_move(node.content,pos); + draw.rectangle(content, null, {color:dbg_colors.content}); + draw.rectangle(boundingbox, null, {color:dbg_colors.boundingbox}); + // draw.rectangle(geometry.rect_move(node.marginbox,pos), dbg_colors.margin); + + // Recursively draw debug for children + if (node[CHILDREN]) { + for (var child of node[CHILDREN]) { + draw_debug_node(child); + } + } + } + + draw_debug_node(tree_root); +} + +clay.print_tree = function print_tree(tree_root, indent = 0) { + var indent_str = ' '.repeat(indent) + var node_type = 'unknown' + + if (tree_root.config.text) { + node_type = 'text' + } else if (tree_root.config.image) { + node_type = 'image' + } else if (tree_root.config.contain) { + if (tree_root.config.contain & layout.contain.column) { + node_type = 'vstack' + } else if (tree_root.config.contain & layout.contain.row) { + node_type = 'hstack' + } else { + node_type = 'container' + } + } else { + node_type = 'node' + } + + log.console(`${indent_str}${node_type} (id: ${tree_root.id})`) + + if (tree_root[CHILDREN] && tree_root[CHILDREN].length > 0) { + log.console(`${indent_str} children: ${tree_root[CHILDREN].length}`) + for (var child of tree_root[CHILDREN]) { + print_tree(child, indent + 1) + } + } else { + log.console(`${indent_str} (no children)`) + } +} + +return clay diff --git a/clay_input.cm b/clay_input.cm new file mode 100644 index 00000000..2ab156a4 --- /dev/null +++ b/clay_input.cm @@ -0,0 +1,110 @@ +// clay_input.cm - Input handling for clay UI +// Separates input concerns from layout/rendering + +var geometry = use('geometry') +var point = use('point') +var clay = use('prosperon/clay') + +var clay_input = {} + +function rect_contains(node, pos) { + var bb = geometry.rect_move(node.boundingbox, node.config.offset || {x: 0, y: 0}) + return geometry.rect_point_inside(bb, pos) +} + +function pointer_enabled(node) { + var p = node.config.pointer_events + if (!p || p == 'auto') return true + if (p == 'none') return false + return true +} + +function should_skip_children(node) { + var p = node.config.pointer_events + if (p == 'box-only') return true + return false +} + +function should_skip_self(node) { + var p = node.config.pointer_events + if (p == 'box-none') return true + return false +} + +function find_path(node, path, pos) { + if (!pointer_enabled(node)) return null + if (!rect_contains(node, pos)) return null + + var next_path = path.concat(node) + + if (node[clay.CHILDREN] && !should_skip_children(node)) { + // Children drawn later should be tested first; reverse if your render order differs + for (var i = node[clay.CHILDREN].length - 1; i >= 0; i--) { + var child = node[clay.CHILDREN][i] + var child_path = find_path(child, next_path, pos) + if (child_path) return child_path + } + } + + if (should_skip_self(node)) return null + return next_path +} + +clay_input.deepest = function deepest(tree_root, pos) { + var path = find_path(tree_root, [], pos) || [] + var deepest = path.length ? path[path.length - 1] : null + return deepest +} + +clay_input.bubble = function bubble(deepest, prop) { + var current = deepest + while (current) { + if (current.config && current.config[prop]) + return current + current = current[clay.PARENT] + } + return null +} + +clay_input.click = function click(tree_root, mousepos, button = 'left') { + var deepest = clay_input.deepest(tree_root, mousepos) + var action_target = clay_input.bubble(deepest, 'action') + if (action_target && action_target.config.action) action_target.config.action() +} + +clay_input.get_actionable = function get_actionable(tree_root) { + var actionable = [] + function walk(node) { + if (node.config.action) actionable.push(node) + if (node[clay.CHILDREN]) + for (var child of node[clay.CHILDREN]) walk(child) + } + walk(tree_root) + return actionable +} + +clay_input.filter = function filter(tree_root, predicate) { + var results = [] + function rec(node) { + if (predicate(node)) results.push(node) + if (node[clay.CHILDREN]) + for (var child of node[clay.CHILDREN]) rec(child) + } + rec(tree_root) + return results +} + +clay_input.find_by_id = function find_by_id(tree_root, id) { + function rec(node) { + if (node.id == id) return node + if (node[clay.CHILDREN]) + for (var child of node[clay.CHILDREN]) { + var f = rec(child) + if (f) return f + } + return null + } + return rec(tree_root) +} + +return clay_input diff --git a/color.cm b/color.cm new file mode 100644 index 00000000..710e6fd0 --- /dev/null +++ b/color.cm @@ -0,0 +1,216 @@ +function tohex(n) { + var s = Math.floor(n).toString(16); + if (s.length == 1) s = "0" + s; + return s.toUpperCase(); +}; + +var Color = { + white: [1, 1, 1], + black: [0, 0, 0], + blue: [0, 0, 1], + green: [0, 1, 0], + yellow: [1, 1, 0], + red: [1, 0, 0], + gray: [0.71, 0.71, 0.71], + cyan: [0, 1, 1], + purple: [0.635, 0.365, 0.89], + orange: [1, 0.565, 0.251], + magenta: [1, 0, 1], +}; + +Color.editor = {}; +Color.editor.ur = Color.green; + +Color.tohtml = function (v) { + var html = v.map(function (n) { + return tohex(n * 255); + }); + return "#" + html.join(""); +}; + +var esc = {}; +esc.reset = "\x1b[0"; +esc.color = function (v) { + var c = v.map(function (n) { + return Math.floor(n * 255); + }); + var truecolor = "\x1b[38;2;" + c.join(";") + ";"; + return truecolor; +}; + +esc.doc = "Functions and constants for ANSI escape sequences."; + +Color.Arkanoid = { + orange: [1, 0.561, 0], + teal: [0, 1, 1], + green: [0, 1, 0], + red: [1, 0, 0], + blue: [0, 0.439, 1], + purple: [1, 0, 1], + yellow: [1, 1, 0], + silver: [0.616, 0.616, 0.616], + gold: [0.737, 0.682, 0], +}; + +Color.Arkanoid.Powerups = { + red: [0.682, 0, 0] /* laser */, + blue: [0, 0, 0.682] /* enlarge */, + green: [0, 0.682, 0] /* catch */, + orange: [0.878, 0.561, 0] /* slow */, + purple: [0.824, 0, 0.824] /* break */, + cyan: [0, 0.682, 1] /* disruption */, + gray: [0.561, 0.561, 0.561] /* 1up */, +}; + +Color.Gameboy = { + darkest: [0.898, 0.42, 0.102], + dark: [0.898, 0.741, 0.102], + light: [0.741, 0.898, 0.102], + lightest: [0.42, 0.898, 0.102], +}; + +Color.Apple = { + green: [0.369, 0.741, 0.243], + yellow: [1, 0.725, 0], + orange: [0.969, 0.51, 0], + red: [0.886, 0.22, 0.22], + purple: [0.592, 0.224, 0.6], + blue: [0, 0.612, 0.875], +}; + +Color.Debug = { + boundingbox: Color.white, + names: [0.329, 0.431, 1], +}; + +Color.Editor = { + grid: [0.388, 1, 0.502], + select: [1, 1, 0.216], + newgroup: [0.471, 1, 0.039], +}; + +/* Detects the format of all colors and munges them into a floating point format */ +Color.normalize = function (c) { + var add_a = function (a) { + var n = this.slice(); + n[3] = a; + return n; + }; + + for (var p of Object.keys(c)) { + if (typeof c[p] != "object") continue; + if (!Array.isArray(c[p])) { + Color.normalize(c[p]); + continue; + } + + // Add alpha channel if not present + if (c[p].length == 3) { + c[p][3] = 1; + } + + // Check if any values are > 1 (meaning they're in 0-255 format) + var needs_conversion = false; + for (var color of c[p]) { + if (color > 1) { + needs_conversion = true; + break; + } + } + + // Convert from 0-255 to 0-1 if needed + if (needs_conversion) { + c[p] = c[p].map(function (x) { + return x / 255; + }); + } + + c[p].alpha = add_a; + } +}; + +Color.normalize(Color); + +var ColorMap = {}; +ColorMap.makemap = function (map) { + var newmap = Object.create(ColorMap); + Object.assign(newmap, map); + return newmap; +}; +ColorMap.Jet = ColorMap.makemap({ + 0: [0, 0, 0.514], + 0.125: [0, 0.235, 0.667], + 0.375: [0.02, 1, 1], + 0.625: [1, 1, 0], + 0.875: [0.98, 0, 0], + 1: [0.502, 0, 0], +}); + +ColorMap.BlueRed = ColorMap.makemap({ + 0: [0, 0, 1], + 1: [1, 0, 0], +}); + +ColorMap.Inferno = ColorMap.makemap({ + 0: [0, 0, 0.016], + 0.13: [0.122, 0.047, 0.282], + 0.25: [0.333, 0.059, 0.427], + 0.38: [0.533, 0.133, 0.416], + 0.5: [0.729, 0.212, 0.333], + 0.63: [0.89, 0.349, 0.2], + 0.75: [0.976, 0.549, 0.039], + 0.88: [0.976, 0.788, 0.196], + 1: [0.988, 1, 0.643], +}); + +ColorMap.Bathymetry = ColorMap.makemap({ + 0: [0.157, 0.102, 0.173], + 0.13: [0.233, 0.192, 0.353], + 0.25: [0.251, 0.298, 0.545], + 0.38: [0.247, 0.431, 0.592], + 0.5: [0.282, 0.557, 0.62], + 0.63: [0.333, 0.682, 0.639], + 0.75: [0.471, 0.808, 0.639], + 0.88: [0.733, 0.902, 0.675], + 1: [0.992, 0.996, 0.8], +}); + +ColorMap.Viridis = ColorMap.makemap({ + 0: [0.267, 0.004, 0.329], + 0.13: [0.278, 0.173, 0.478], + 0.25: [0.231, 0.318, 0.545], + 0.38: [0.173, 0.443, 0.557], + 0.5: [0.129, 0.565, 0.553], + 0.63: [0.153, 0.678, 0.506], + 0.75: [0.361, 0.784, 0.388], + 0.88: [0.667, 0.863, 0.196], + 1: [0.992, 0.906, 0.145], +}); + +Color.normalize(ColorMap); + +ColorMap.sample = function (t, map = this) { + if (t < 0) return map[0]; + if (t > 1) return map[1]; + + var lastkey = 0; + for (var key of Object.keys(map).sort()) { + if (t < key) { + var b = map[key]; + var a = map[lastkey]; + var tt = (key - lastkey) * t; + return a.lerp(b, tt); + } + lastkey = key; + } + return map[1]; +}; + +ColorMap.doc = { + sample: "Sample a given colormap at the given percentage (0 to 1).", +}; + +Color.maps = ColorMap +Color.utils = esc + +return Color diff --git a/controller.cm b/controller.cm new file mode 100644 index 00000000..f1b8d32d --- /dev/null +++ b/controller.cm @@ -0,0 +1,345 @@ +var input = use('input') +return {} + +var downkeys = {}; + +function keyname(key) +{ + var str = input.keyname(key); + return str.toLowerCase(); +} + +function modstr(mod = input.keymod()) { + var s = ""; + if (mod.ctrl) s += "C-"; + if (mod.alt) s += "M-"; + if (mod.super) s += "S-"; + return s; +} + +prosperon.on('key_down', function key_down(e) { + downkeys[e.key] = true; + var emacs = modstr(e.mod) + keyname(e.key); + if (e.repeat) player[0].raw_input(emacs, "rep"); + else player[0].raw_input(emacs, "pressed"); +}) + +prosperon.on('quit', function() { + os.exit(0); +}) + +prosperon.on('key_up', function key_up(e) { + delete downkeys[e.key]; + var emacs = modstr(e.mod) + keyname(e.key); + player[0].raw_input(emacs, "released"); +}) + +prosperon.on('drop_file', function (path) { + player[0].raw_input("drop", "pressed", path); +}) + +var mousepos = [0, 0]; + +prosperon.on('text_input', function (e) { + player[0].raw_input("char", "pressed", e.text); +}) + +prosperon.on('mouse_motion', function (e) +{ + mousepos = e.pos; + player[0].mouse_input("move", e.pos, e.d_pos); +}) + +prosperon.on('mouse_wheel', function mousescroll(e) { + player[0].mouse_input(modstr() + "scroll", e.scroll); +}) + +prosperon.on('mouse_button_down', function(e) { + player[0].mouse_input(modstr() + e.button, "pressed"); + input.mouse.buttons[e.button] = true +}) + +prosperon.on('mouse_button_up', function(e) { + player[0].mouse_input(modstr() + e.button, "released"); + input.mouse.buttons[e.button] = false +}) + +input.mouse = {}; +input.mouse.screenpos = function mouse_screenpos() { + return mousepos.slice(); +}; +input.mouse.worldpos = function mouse_worldpos() { + return prosperon.camera.screen2world(mousepos); +}; +input.mouse.viewpos = function mouse_viewpos() +{ + var world = input.mouse.worldpos(); + + return mousepos.slice(); +} +input.mouse.disabled = function mouse_disabled() { + input.mouse_mode(1); +}; +input.mouse.normal = function mouse_normal() { + input.mouse_mode(0); +}; +input.mouse.mode = function mouse_mode(m) { + if (input.mouse.custom[m]) input.cursor_img(input.mouse.custom[m]); + else input.mouse_cursor(m); +}; +input.mouse.buttons = { + 0:false, + 1:false, + 2:false +} + +input.mouse.set_custom_cursor = function mouse_cursor(img, mode = input.mouse.cursor.default) { + if (!img) delete input.mouse.custom[mode]; + else { + input.cursor_img(img); + input.mouse.custom[mode] = img; + } +}; +input.mouse.doc = {}; +input.mouse.doc.pos = "The screen position of the mouse."; +input.mouse.doc.worldpos = "The position in the game world of the mouse."; +input.mouse.disabled.doc = "Set the mouse to hidden. This locks it to the game and hides it, but still provides movement and click events."; +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()]; + return null; +}; + +input.print_pawn_kbm = function (pawn) { + if (!("inputs" in pawn)) return; + var str = ""; + for (var key in pawn.inputs) { + if (!pawn.inputs[key].doc) continue; + str += `${key} | ${pawn.inputs[key].doc}\n`; + } + return str; +}; + +var joysticks = {}; + +joysticks["wasd"] = { + uy: "w", + dy: "s", + ux: "d", + dx: "a", +}; + +input.procdown = function procdown() { + for (var k in downkeys) player[0].raw_input(keyname(k), "down"); + + for (var i in joysticks) { + var joy = joysticks[i]; + var x = joy.ux - joy.dx; + var y = joy.uy - joy.dy; + player[0].joy_input(i, joysticks[i]); + } +}; + +input.print_md_kbm = function print_md_kbm(pawn) { + if (!("inputs" in pawn)) return; + + var str = ""; + str += "|control|description|\n|---|---|\n"; + + for (var key in pawn.inputs) { + str += `|${key}|${pawn.inputs[key].doc}|`; + str += "\n"; + } + + return str; +}; + +input.has_bind = function (pawn, bind) { + return typeof pawn.inputs?.[bind] == "function"; +}; + +input.action = { + add_new(name) { + var action = Object.create(input.action); + action.name = name; + action.inputs = []; + this.actions.push(action); + + return action; + }, + actions: [], +}; + +input.tabcomplete = function tabcomplete(val, list) { + if (!val) return val; + list = filter(x => x.startsWith(val)) + + if (list.length == 1) { + return list[0]; + } + + var ret = null; + var i = val.length; + while (!ret && list.length != 0) { + var char = list[0][i]; + if ( + !list.every(function (x) { + return x[i] == char; + }) + ) + ret = list[0].slice(0, i); + else { + i++; + list = list.filter(x => x.length-1 > i) + } + } + + return ret ? ret : val; +}; + +/* May be a human player; may be an AI player */ + +/* + 'block' on a pawn's input blocks any input from reaching below for the +*/ + +var Player = { + players: [], + input(fn, ...args) { + this.pawns.forEach(x => x[fn]?.(...args)); + }, + + mouse_input(type, ...args) { + for (var pawn of [...this.pawns].reverse()) { + if (typeof pawn.inputs?.mouse?.[type] == "function") { + pawn.inputs.mouse[type].call(pawn, ...args); + pawn.inputs.post?.call(pawn); + if (!pawn.inputs.fallthru) return; + } + } + }, + + char_input(c) { + for (var pawn of [...this.pawns].reverse()) { + if (typeof pawn.inputs?.char == "function") { + pawn.inputs.char.call(pawn, c); + pawn.inputs.post?.call(pawn); + if (!pawn.inputs.fallthru) return; + } + } + }, + + joy_input(name, joystick) { + for (var pawn of [...this.pawns].reverse()) { + if (!pawn.inputs) return; + if (!pawn.inputs.joystick) return; + if (!pawn.inputs.joystick[name]) return; + + var x = 0; + if (input.keyboard.down(joystick.ux)) x++; + if (input.keyboard.down(joystick.dx)) x--; + var y = 0; + if (input.keyboard.down(joystick.uy)) y++; + if (input.keyboard.down(joystick.dy)) y--; + + pawn.inputs.joystick[name](x, y); + } + }, + + raw_input(cmd, state, ...args) { + for (var pawn of [...this.pawns].reverse()) { + var inputs = pawn.inputs; + + if (!inputs[cmd]) { + if (inputs.block) return; + continue; + } + + var fn = null; + + switch (state) { + case "pressed": + fn = inputs[cmd]; + break; + case "rep": + fn = inputs[cmd].rep ? inputs[cmd] : null; + break; + case "released": + fn = inputs[cmd].released; + break; + case "down": + if (typeof inputs[cmd].down == "function") fn = inputs[cmd].down; + else if (inputs[cmd].down) fn = inputs[cmd]; + } + + var consumed = false; + if (typeof fn == "function") { + fn.call(pawn, ...args); + consumed = true; + } + if (state == "released") inputs.release_post?.call(pawn); + if (inputs.block) return; + if (consumed) return; + } + }, + + obj_controlled(obj) { + for (var p in Player.players) { + if (p.pawns.has(obj)) return true; + } + + return false; + }, + + print_pawns() { + [...this.pawns].reverse().forEach(x => log.console(x)) + }, + + create() { + var n = Object.create(this); + n.pawns = new Set() + n.gamepads = []; + this.players.push(n); + this[this.players.length - 1] = n; + return n; + }, + + control(pawn) { + if (!pawn) + return + + if (!pawn.inputs) + throw new Error("attempted to control a pawn without any input object."); + + this.pawns.add(pawn); + }, + + uncontrol(pawn) { + this.pawns.delete(pawn) + }, +}; + +input.do_uncontrol = function input_do_uncontrol(pawn) { + if (!pawn.inputs) return; + Player.players.forEach(function (p) { + p.pawns.delete(pawn) + }); +}; + +//for (var i = 0; i < 4; i++) +Player.create(); + +Player.control.doc = "Control a provided object, if the object has an 'inputs' object."; +Player.uncontrol.doc = "Uncontrol a previously controlled object."; +Player.print_pawns.doc = "Print out a list of the current pawn control stack."; +Player.doc = {}; +Player.doc.players = "A list of current players."; + +var player = Player; + +input.player = Player + +return input diff --git a/device.cm b/device.cm new file mode 100644 index 00000000..48c12e35 --- /dev/null +++ b/device.cm @@ -0,0 +1,35 @@ +// helpful render devices. width and height in pixels; diagonal in inches. +return { + pc: { width: 1920, height: 1080 }, + macbook_m2: { width: 2560, height: 1664, diagonal: 13.6 }, + ds_top: { width: 400, height: 240, diagonal: 3.53 }, + ds_bottom: { width: 320, height: 240, diagonal: 3.02 }, + playdate: { width: 400, height: 240, diagonal: 2.7 }, + switch: { width: 1280, height: 720, diagonal: 6.2 }, + switch_lite: { width: 1280, height: 720, diagonal: 5.5 }, + switch_oled: { width: 1280, height: 720, diagonal: 7 }, + dsi: { width: 256, height: 192, diagonal: 3.268 }, + ds: { width: 256, height: 192, diagonal: 3 }, + dsixl: { width: 256, height: 192, diagonal: 4.2 }, + ipad_air_m2: { width: 2360, height: 1640, diagonal: 11.97 }, + iphone_se: { width: 1334, height: 750, diagonal: 4.7 }, + iphone_12_pro: { width: 2532, height: 1170, diagonal: 6.06 }, + iphone_15: { width: 2556, height: 1179, diagonal: 6.1 }, + gba: { width: 240, height: 160, diagonal: 2.9 }, + gameboy: { width: 160, height: 144, diagonal: 2.48 }, + gbc: { width: 160, height: 144, diagonal: 2.28 }, + steamdeck: { width: 1280, height: 800, diagonal: 7 }, + vita: { width: 960, height: 544, diagonal: 5 }, + psp: { width: 480, height: 272, diagonal: 4.3 }, + imac_m3: { width: 4480, height: 2520, diagonal: 23.5 }, + macbook_pro_m3: { width: 3024, height: 1964, diagonal: 14.2 }, + ps1: { width: 320, height: 240, diagonal: 5 }, + ps2: { width: 640, height: 480 }, + snes: { width: 256, height: 224 }, + gamecube: { width: 640, height: 480 }, + n64: { width: 320, height: 240 }, + c64: { width: 320, height: 200 }, + macintosh: { width: 512, height: 342 }, + gamegear: { width: 160, height: 144, diagonal: 3.2 } +}; + diff --git a/draw2d.cm b/draw2d.cm new file mode 100644 index 00000000..f92fdc0c --- /dev/null +++ b/draw2d.cm @@ -0,0 +1,251 @@ +var math = use('math') +var color = use('color') +var gamestate = use('gamestate') + +var draw = {} + +var current_list = [] + +// Clear current list +draw.clear = function() { + current_list = [] +} + +// Get commands from current list +draw.get_commands = function() { + return current_list +} + +// Helper to add a command +function add_command(type, data) { + data.cmd = type + current_list.push(data) +} + +// Default geometry definitions +var ellipse_def = { + start: 0, + end: 1, + mode: 'fill', + thickness: 1, +} + +var line_def = { + thickness: 1, + cap:"butt", +} + +var rect_def = { + thickness:1, + radius: 0 +} + +var image_info = { + tile_x: false, + tile_y: false, + flip_x: false, + flip_y: false, + mode: 'linear' +} + +var circle_def = { + inner_radius:1, // percentage: 1 means filled circle + start:0, + end: 1, +} + +// Drawing functions +draw.point = function(pos, size, opt = {}, material) { + add_command("draw_point", { + pos: pos, + size: size, + opt: opt, + material: material + }) +} + +draw.ellipse = function(pos, radii, defl, material) { + var opt = defl ? {...ellipse_def, ...defl} : ellipse_def + if (opt.thickness <= 0) opt.thickness = Math.max(radii[0], radii[1]) + + add_command("draw_ellipse", { + pos: pos, + radii: radii, + opt: opt, + material: material + }) +} + +draw.line = function(points, defl, material) +{ + var opt = defl ? {...line_def, ...defl} : line_def + + add_command("draw_line", { + points: points, + opt: opt, + material: material + }) +} + +draw.cross = function render_cross(pos, size, defl, material) { + var a = [pos.add([0, size]), pos.add([0, -size])] + var b = [pos.add([size, 0]), pos.add([-size, 0])] + draw.line(a, defl, material) + draw.line(b, defl, material) +} + +draw.arrow = function render_arrow(start, end, wingspan = 4, wingangle = 10, defl, material) { + var dir = math.norm(end.sub(start)) + var wing1 = [math.rotate(dir, wingangle).scale(wingspan).add(end), end] + var wing2 = [math.rotate(dir, -wingangle).scale(wingspan).add(end), end] + draw.line([start, end], defl, material) + draw.line(wing1, defl, material) + draw.line(wing2, defl, material) +} + +draw.rectangle = function render_rectangle(rect, defl, material = {color:{r:1,g:1,b:1,a:1}}) { + var opt = defl ? {...rect_def, ...defl} : rect_def + + add_command("draw_rect", { + rect, + opt, + material + }) +} + +var slice9_info = { + tile_top:true, + tile_bottom:true, + tile_left:true, + tile_right:true, + tile_center_x:true, + tile_center_right:true, +} + +draw.slice9 = function slice9(image, rect = [0,0], slice = 0, info = slice9_info, material) { + if (!image) throw Error('Need an image to render.') + + add_command("draw_slice9", { + image, + rect, + slice, + info, + material + }) +} + +draw.image = function image(image, rect, scale = {x:1,y:1}, anchor, shear, info, material) { + if (!rect) throw Error('Need rectangle to render image.') + if (!image) throw Error('Need an image to render.') + + if (!('x' in rect && 'y' in rect)) throw Error('Must provide X and Y for image.') + + add_command("draw_image", { + image, + rect, + scale, + anchor, + shear, + info, + material + }) +} + +draw.circle = function render_circle(pos, radius, defl, material) { + draw.ellipse(pos, [radius,radius], defl, material) +} + +// wrap is the width before wrapping +// config is any additional config to pass to the text renderer +var text_base_config = { + align: 'left', // left, right, center, justify + break: 'word', // word, character +} +draw.text = function text(text, pos, font = 'fonts/c64.8', color = {r:1,g:1,b:1,a:1}, wrap = 0, config = {}) { + config.align ??= text_base_config.align + config.break ??= text_base_config.break + + add_command("draw_text", { + text, + pos, + font, + wrap, + material: {color}, + config + }) +} + +draw.grid = function grid(rect, spacing, thickness = 1, offset = {x: 0, y: 0}, material) { + if (!rect || rect.x == null || rect.y == null || + 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') { + throw Error('Grid requires spacing with x and y') + } + + var left = rect.x + var right = rect.x + rect.width + var top = rect.y + var bottom = rect.y + rect.height + + // Apply offset and align to grid + var start_x = Math.floor((left - offset.x) / spacing.x) * spacing.x + offset.x + var end_x = Math.ceil((right - offset.x) / spacing.x) * spacing.x + offset.x + var start_y = Math.floor((top - offset.y) / spacing.y) * spacing.y + offset.y + var end_y = Math.ceil((bottom - offset.y) / spacing.y) * spacing.y + offset.y + + // Draw vertical lines + for (var x = start_x; x <= end_x; x += spacing.x) { + if (x >= left && x <= right) { + var line_top = Math.max(top, start_y) + var line_bottom = Math.min(bottom, end_y) + draw.line([[x, line_top], [x, line_bottom]], {thickness: thickness}, material) + } + } + + // Draw horizontal lines + for (var y = start_y; y <= end_y; y += spacing.y) { + if (y >= top && y <= bottom) { + var line_left = Math.max(left, start_x) + var line_right = Math.min(right, end_x) + draw.line([[line_left, y], [line_right, y]], {thickness: thickness}, material) + } + } +} + +draw.scissor = function(rect) +{ + var screen_rect = null + if (rect && gamestate.camera) { + var bottom_left = gamestate.camera.world_to_window(rect.x, rect.y) + var top_right = gamestate.camera.world_to_window(rect.x + rect.width, rect.y + rect.height) + var screen_left = bottom_left.x + var screen_top = bottom_left.y + var screen_right = top_right.x + var screen_bottom = top_right.y + + screen_rect = { + x: Math.round(screen_left), + y: Math.round(screen_top), + width: Math.round(screen_right - screen_left), + height: Math.round(screen_bottom - screen_top) + } + + // TODO: must be a better way than manually inverting here. Some camera specific function. + var sensor = gamestate.camera.sensor() + screen_rect.y = sensor.height - screen_rect.y - screen_rect.height + } + + current_list.push({ + cmd: "scissor", + rect: screen_rect + }) +} + +draw.add_command = function(cmd) +{ + current_list.push(cmd) +} + +return draw \ No newline at end of file diff --git a/ease.cm b/ease.cm new file mode 100644 index 00000000..389eb354 --- /dev/null +++ b/ease.cm @@ -0,0 +1,148 @@ +var Ease = { + linear(t) { + return t + }, + in(t) { + return t * t + }, + out(t) { + var d = 1 - t + return 1 - d * d + }, + inout(t) { + var d = -2 * t + 2 + return t < 0.5 ? 2 * t * t : 1 - (d * d) / 2 + }, +} + +function make_easing_fns(num) { + var obj = {} + + obj.in = function (t) { + return Math.pow(t, num) + } + + obj.out = function (t) { + return 1 - Math.pow(1 - t, num) + } + + var mult = Math.pow(2, num - 1) + obj.inout = function (t) { + return t < 0.5 ? mult * Math.pow(t, num) : 1 - Math.pow(-2 * t + 2, num) / 2 + } + + return obj +} + +Ease.quad = make_easing_fns(2) +Ease.cubic = make_easing_fns(3) +Ease.quart = make_easing_fns(4) +Ease.quint = make_easing_fns(5) + +Ease.expo = { + in(t) { + return t == 0 ? 0 : Math.pow(2, 10 * t - 10) + }, + out(t) { + return t == 1 ? 1 : 1 - Math.pow(2, -10 * t) + }, + inout(t) { + return t == 0 + ? 0 + : t == 1 + ? 1 + : t < 0.5 + ? Math.pow(2, 20 * t - 10) / 2 + : (2 - Math.pow(2, -20 * t + 10)) / 2 + }, +} + +Ease.bounce = { + in(t) { + return 1 - this.out(1 - t) + }, + out(t) { + var n1 = 7.5625 + var d1 = 2.75 + if (t < 1 / d1) { + return n1 * t * t + } else if (t < 2 / d1) { + return n1 * (t -= 1.5 / d1) * t + 0.75 + } else if (t < 2.5 / d1) { + return n1 * (t -= 2.25 / d1) * t + 0.9375 + } else return n1 * (t -= 2.625 / d1) * t + 0.984375 + }, + inout(t) { + return t < 0.5 ? (1 - this.out(1 - 2 * t)) / 2 : (1 + this.out(2 * t - 1)) / 2 + }, +} + +Ease.sine = { + in(t) { + return 1 - Math.cos((t * Math.PI) / 2) + }, + out(t) { + return Math.sin((t * Math.PI) / 2) + }, + inout(t) { + return -(Math.cos(Math.PI * t) - 1) / 2 + }, +} + +Ease.elastic = { + in(t) { + return t == 0 + ? 0 + : t == 1 + ? 1 + : -Math.pow(2, 10 * t - 10) * + Math.sin((t * 10 - 10.75) * this.c4) + }, + out(t) { + return t == 0 + ? 0 + : t == 1 + ? 1 + : Math.pow(2, -10 * t) * + Math.sin((t * 10 - 0.75) * this.c4) + + 1 + }, + inout(t) { + return t == 0 + ? 0 + : t == 1 + ? 1 + : t < 0.5 + ? -(Math.pow(2, 20 * t - 10) * Math.sin((20 * t - 11.125) * this.c5)) / 2 + : (Math.pow(2, -20 * t + 10) * Math.sin((20 * t - 11.125) * this.c5)) / 2 + 1 + }, +} + +Ease.elastic.c4 = (2 * Math.PI) / 3 +Ease.elastic.c5 = (2 * Math.PI) / 4.5 + +Ease.zoom = { + // Creates a smooth zoom that maintains constant perceptual speed + // ratio is the zoom factor (e.g., 10 for 10x zoom) + smooth(ratio) { + return function(t) { + if (t == 0) return 0 + if (t == 1) return 1 + if (Math.abs(ratio - 1) < 0.001) return t + // Position interpolation formula: (r^t - 1) / (r - 1) + return (Math.pow(ratio, t) - 1) / (ratio - 1) + } + }, + // Exponential interpolation for zoom values + // Interpolates in logarithmic space for smooth visual zoom + exp(startZoom, endZoom) { + return function(t) { + if (t == 0) return startZoom + if (t == 1) return endZoom + // Scale := Exp(LinearInterpolate(Ln(Scale1), Ln(Scale2), t)) + return Math.exp(Math.log(startZoom) + t * (Math.log(endZoom) - Math.log(startZoom))) + } + } +} + +return Ease \ No newline at end of file diff --git a/examples/bunnymark/bunny.png b/examples/bunnymark/bunny.png new file mode 100644 index 00000000..79c31675 Binary files /dev/null and b/examples/bunnymark/bunny.png differ diff --git a/examples/bunnymark/config.cm b/examples/bunnymark/config.cm new file mode 100644 index 00000000..58000d55 --- /dev/null +++ b/examples/bunnymark/config.cm @@ -0,0 +1,5 @@ +return { + title:"Bunnymark", + width:1200, + height:600, +} diff --git a/examples/bunnymark/main.ce b/examples/bunnymark/main.ce new file mode 100644 index 00000000..56a779f8 --- /dev/null +++ b/examples/bunnymark/main.ce @@ -0,0 +1,70 @@ +var draw = use('draw2d') +var render = use('render') +var graphics = use('graphics') +var sprite = use('sprite') +var geom = use('geometry') +var input = use('controller') +var config = use('config') +var color = use('color') + +var bunnyTex = graphics.texture("bunny") + +// We'll store our bunnies in an array of objects: { x, y, vx, vy } +var bunnies = [] + +// Start with some initial bunnies: +for (var i = 0; i < 100; i++) { + bunnies.push({ + x: Math.random() * config.width, + y: Math.random() * config.height, + vx: (Math.random() * 300) - 150, + vy: (Math.random() * 300) - 150 + }) +} + +var fpsSamples = [] +var fpsAvg = 0 + +this.update = function(dt) { + // Compute FPS average over the last 60 frames: + var currentFPS = 1 / dt + fpsSamples.push(currentFPS) + if (fpsSamples.length > 60) fpsSamples.shift() + var sum = 0 + for (var f of fpsSamples) sum += f + fpsAvg = sum / fpsSamples.length + + // If left mouse is down, spawn some more bunnies: + var mouse = input.mousestate() + if (mouse.left) + for (var i = 0; i < 50; i++) { + bunnies.push({ + x: mouse.x, + y: mouse.y, + vx: (Math.random() * 300) - 150, + vy: (Math.random() * 300) - 150 + }) + } + + // Update bunny positions and bounce them inside the screen: + for (var i = 0; i < bunnies.length; i++) { + var b = bunnies[i] + b.x += b.vx * dt + b.y += b.vy * dt + + // Bounce off left/right edges + if (b.x < 0) { b.x = 0; b.vx = -b.vx } + else if (b.x > config.width) { b.x = config.width; b.vx = -b.vx } + + // Bounce off bottom/top edges + if (b.y < 0) { b.y = 0; b.vy = -b.vy } + else if (b.y > config.height) { b.y = config.height; b.vy = -b.vy } + } +} + +this.hud = function() { + draw.images(bunnyTex, bunnies) + + var msg = 'FPS: ' + fpsAvg.toFixed(2) + ' Bunnies: ' + bunnies.length + draw.text(msg, {x:0, y:0, width:config.width, height:40}, null, 0, color.white, 0) +} diff --git a/examples/chess/black_bishop.png b/examples/chess/black_bishop.png new file mode 100644 index 00000000..9725f5ed Binary files /dev/null and b/examples/chess/black_bishop.png differ diff --git a/examples/chess/black_king.png b/examples/chess/black_king.png new file mode 100644 index 00000000..720cfa0f Binary files /dev/null and b/examples/chess/black_king.png differ diff --git a/examples/chess/black_knight.png b/examples/chess/black_knight.png new file mode 100644 index 00000000..c534b8d6 Binary files /dev/null and b/examples/chess/black_knight.png differ diff --git a/examples/chess/black_pawn.png b/examples/chess/black_pawn.png new file mode 100644 index 00000000..b90295d7 Binary files /dev/null and b/examples/chess/black_pawn.png differ diff --git a/examples/chess/black_queen.png b/examples/chess/black_queen.png new file mode 100644 index 00000000..b238f175 Binary files /dev/null and b/examples/chess/black_queen.png differ diff --git a/examples/chess/black_rook.png b/examples/chess/black_rook.png new file mode 100644 index 00000000..f1f6d2e9 Binary files /dev/null and b/examples/chess/black_rook.png differ diff --git a/examples/chess/chess.ce b/examples/chess/chess.ce new file mode 100644 index 00000000..0915eb0c --- /dev/null +++ b/examples/chess/chess.ce @@ -0,0 +1,395 @@ +/* main.js – runs the demo with your prototype-based grid */ + +var json = use('json') +var draw2d = use('prosperon/draw2d') + +var blob = use('blob') + +/*──── import our pieces + systems ───────────────────────────────────*/ +var Grid = use('grid'); // your new ctor +var MovementSystem = use('movement').MovementSystem; +var startingPos = use('pieces').startingPosition; +var rules = use('rules'); + +/*──── build board ───────────────────────────────────────────────────*/ +var grid = new Grid(8, 8); +grid.width = 8; // (the ctor didn't store them) +grid.height = 8; + +var mover = new MovementSystem(grid, rules); +startingPos(grid); + +/*──── networking and game state ─────────────────────────────────────*/ +var gameState = 'waiting'; // 'waiting', 'searching', 'server_waiting', 'connected' +var isServer = false; +var opponent = null; +var myColor = null; // 'white' or 'black' +var isMyTurn = false; + +function updateTitle() { + var title = "Misty Chess - "; + + switch(gameState) { + case 'waiting': + title += "Press S to start server or J to join"; + break; + case 'searching': + title += "Searching for server..."; + break; + case 'server_waiting': + title += "Waiting for player to join..."; + break; + case 'connected': + if (myColor) { + title += (mover.turn == myColor ? "Your turn (" + myColor + ")" : "Opponent's turn (" + mover.turn + ")"); + } else { + title += mover.turn + " turn"; + } + break; + } + + log.console(title) +} + +// Initialize title +updateTitle(); + +/*──── mouse → click-to-move ─────────────────────────────────────────*/ +var selectPos = null; +var hoverPos = null; +var holdingPiece = false; + +var opponentMousePos = null; +var opponentHoldingPiece = false; +var opponentSelectPos = null; + +function handleMouseButtonDown(e) { + if (e.which != 0) return; + + // Don't allow piece selection unless we have an opponent + if (gameState != 'connected' || !opponent) return; + + var mx = e.mouse.x; + var my = e.mouse.y; + + var c = [Math.floor(mx / 60), Math.floor(my / 60)]; + if (!grid.inBounds(c)) return; + + var cell = grid.at(c); + if (cell.length && cell[0].colour == mover.turn) { + selectPos = c; + holdingPiece = true; + // Send pickup notification to opponent + if (opponent) { + send(opponent, { + type: 'piece_pickup', + pos: c + }); + } + } else { + selectPos = null; + } +} + +function handleMouseButtonUp(e) { + if (e.which != 0 || !holdingPiece || !selectPos) return; + + // Don't allow moves unless we have an opponent and it's our turn + if (gameState != 'connected' || !opponent || !isMyTurn) { + holdingPiece = false; + return; + } + + var mx = e.mouse.x; + var my = e.mouse.y; + + var c = [Math.floor(mx / 60), Math.floor(my / 60)]; + if (!grid.inBounds(c)) { + holdingPiece = false; + return; + } + + if (mover.tryMove(grid.at(selectPos)[0], c)) { + log.console("Made move from", selectPos, "to", c); + // Send move to opponent + log.console("Sending move to opponent:", opponent); + send(opponent, { + type: 'move', + from: selectPos, + to: c + }); + isMyTurn = false; // It's now opponent's turn + log.console("Move sent, now opponent's turn"); + selectPos = null; + updateTitle(); + } + + holdingPiece = false; + + // Send piece drop notification to opponent + if (opponent) { + send(opponent, { + type: 'piece_drop' + }); + } +} + +function handleMouseMotion(e) { + var mx = e.pos.x; + var my = e.pos.y; + + var c = [Math.floor(mx / 60), Math.floor(my / 60)]; + if (!grid.inBounds(c)) { + hoverPos = null; + return; + } + + hoverPos = c; + + // Send mouse position to opponent in real-time + if (opponent && gameState == 'connected') { + send(opponent, { + type: 'mouse_move', + pos: c, + holding: holdingPiece, + selectPos: selectPos + }); + } +} + +function handleKeyDown(e) { + // S key - start server + if (e.scancode == 22 && gameState == 'waiting') { // S key + startServer(); + } + // J key - join server + else if (e.scancode == 13 && gameState == 'waiting') { // J key + joinServer(); + } +} + +/*──── drawing helpers ───────────────────────────────────────────────*/ +/* ── constants ─────────────────────────────────────────────────── */ +var S = 60; // square size in px +var light = [0.93,0.93,0.93,1]; +var dark = [0.25,0.25,0.25,1]; +var allowedColor = [1.0, 0.84, 0.0, 1.0]; // Gold for allowed moves +var myMouseColor = [0.0, 1.0, 0.0, 1.0]; // Green for my mouse +var opponentMouseColor = [1.0, 0.0, 0.0, 1.0]; // Red for opponent mouse + +/* ── draw one 8×8 chess board ──────────────────────────────────── */ +function drawBoard() { + for (var y = 0; y < 8; ++y) + for (var x = 0; x < 8; ++x) { + var isMyHover = hoverPos && hoverPos[0] == x && hoverPos[1] == y; + var isOpponentHover = opponentMousePos && opponentMousePos[0] == x && opponentMousePos[1] == y; + var isValidMove = selectPos && holdingPiece && isValidMoveForTurn(selectPos, [x, y]); + + var color = ((x+y)&1) ? dark : light; + + if (isValidMove) { + color = allowedColor; // Gold for allowed moves + } else if (isMyHover && !isOpponentHover) { + color = myMouseColor; // Green for my mouse + } else if (isOpponentHover) { + color = opponentMouseColor; // Red for opponent mouse + } + + draw2d.rectangle( + { x: x*S, y: y*S, width: S, height: S }, + { thickness: 0 }, + { color: color } + ); + } +} + +function isValidMoveForTurn(from, to) { + if (!grid.inBounds(to)) return false; + + var piece = grid.at(from)[0]; + if (!piece) return false; + + // Check if the destination has a piece of the same color + var destCell = grid.at(to); + if (destCell.length && destCell[0].colour == piece.colour) { + return false; + } + + return rules.canMove(piece, from, to, grid); +} + +/* ── draw every live piece ─────────────────────────────────────── */ +function drawPieces() { + grid.each(function (piece) { + if (piece.captured) return; + + // Skip drawing the piece being held (by me or opponent) + if (holdingPiece && selectPos && + piece.coord[0] == selectPos[0] && + piece.coord[1] == selectPos[1]) { + return; + } + + // Skip drawing the piece being held by opponent + if (opponentHoldingPiece && opponentSelectPos && + piece.coord[0] == opponentSelectPos[0] && + piece.coord[1] == opponentSelectPos[1]) { + return; + } + + var r = { x: piece.coord[0]*S, y: piece.coord[1]*S, + width:S, height:S }; + + draw2d.image(piece.sprite, r); + }); + + // Draw the held piece at the mouse position if we're holding one + if (holdingPiece && selectPos && hoverPos) { + var piece = grid.at(selectPos)[0]; + if (piece) { + var r = { x: hoverPos[0]*S, y: hoverPos[1]*S, + width:S, height:S }; + + draw2d.image(piece.sprite, r); + } + } + + // Draw opponent's held piece if they're dragging one + if (opponentHoldingPiece && opponentSelectPos && opponentMousePos) { + var opponentPiece = grid.at(opponentSelectPos)[0]; + if (opponentPiece) { + var r = { x: opponentMousePos[0]*S, y: opponentMousePos[1]*S, + width:S, height:S }; + + // Draw with slight transparency to show it's the opponent's piece + draw2d.image(opponentPiece.sprite, r); + } + } +} + +function update(dt) +{ + return {} +} + +function draw() +{ + draw2d.clear() + drawBoard() + drawPieces() + return draw2d.get_commands() +} + +function startServer() { + gameState = 'server_waiting'; + isServer = true; + myColor = 'white'; + isMyTurn = true; + updateTitle(); + + $_.portal(e => { + log.console("Portal received contact message"); + // Reply with this actor to establish connection + log.console (json.encode($_)) + send(e, $_); + log.console("Portal replied with server actor"); + }, 5678); +} + +function joinServer() { + gameState = 'searching'; + updateTitle(); + + function contact_fn(actor, reason) { + log.console("CONTACTED!", actor ? "SUCCESS" : "FAILED", reason); + if (actor) { + opponent = actor; + log.console("Connection established with server, sending join request"); + + // Send a greet message with our actor object + send(opponent, { + type: 'greet', + client_actor: $_ + }); + } else { + log.console(`Failed to connect: ${json.encode(reason)}`); + gameState = 'waiting'; + updateTitle(); + } + } + + $_.contact(contact_fn, { + address: "192.168.0.149", + port: 5678 + }); +} + +$_.receiver(e => { + if (e.kind == 'update') + send(e, update(e.dt)) + else if (e.kind == 'draw') + send(e, draw()) + else if (e.type == 'game_start' || e.type == 'move' || e.type == 'greet') + log.console("Receiver got message:", e.type, e); + + if (e.type == 'greet') { + log.console("Server received greet from client"); + // Store the client's actor object for ongoing communication + opponent = e.client_actor; + log.console("Stored client actor:", json.encode(opponent)); + gameState = 'connected'; + updateTitle(); + + // Send game_start to the client + log.console("Sending game_start to client"); + send(opponent, { + type: 'game_start', + your_color: 'black' + }); + log.console("game_start message sent to client"); + } + else if (e.type == 'game_start') { + log.console("Game starting, I am:", e.your_color); + myColor = e.your_color; + isMyTurn = (myColor == 'white'); + gameState = 'connected'; + updateTitle(); + } else if (e.type == 'move') { + log.console("Received move from opponent:", e.from, "to", e.to); + // Apply opponent's move + var fromCell = grid.at(e.from); + if (fromCell.length) { + var piece = fromCell[0]; + if (mover.tryMove(piece, e.to)) { + isMyTurn = true; // It's now our turn + updateTitle(); + log.console("Applied opponent move, now my turn"); + } else { + log.console("Failed to apply opponent move"); + } + } else { + log.console("No piece found at from position"); + } + } else if (e.type == 'mouse_move') { + // Update opponent's mouse position + opponentMousePos = e.pos; + opponentHoldingPiece = e.holding; + opponentSelectPos = e.selectPos; + } else if (e.type == 'piece_pickup') { + // Opponent picked up a piece + opponentSelectPos = e.pos; + opponentHoldingPiece = true; + } else if (e.type == 'piece_drop') { + // Opponent dropped their piece + opponentHoldingPiece = false; + opponentSelectPos = null; + } else if (e.type == 'mouse_button_down') { + handleMouseButtonDown(e) + } else if (e.type == 'mouse_button_up') { + handleMouseButtonUp(e) + } else if (e.type == 'mouse_motion') { + handleMouseMotion(e) + } else if (e.type == 'key_down') { + handleKeyDown(e) + } +}) diff --git a/examples/chess/config.cm b/examples/chess/config.cm new file mode 100644 index 00000000..d50f71be --- /dev/null +++ b/examples/chess/config.cm @@ -0,0 +1,9 @@ +// Chess game configuration for Moth framework +return { + title: "Chess", + resolution: { width: 480, height: 480 }, + internal_resolution: { width: 480, height: 480 }, + fps: 60, + clearColor: [22/255, 120/255, 194/255, 1], + mode: 'stretch' // No letterboxing for chess +}; \ No newline at end of file diff --git a/examples/chess/grid.cm b/examples/chess/grid.cm new file mode 100644 index 00000000..c6cdffa4 --- /dev/null +++ b/examples/chess/grid.cm @@ -0,0 +1,69 @@ +function grid(w, h) { + this.width = w; + this.height = h; + // create a height×width array of empty lists + this.cells = new Array(h); + for (let y = 0; y < h; y++) { + this.cells[y] = new Array(w); + for (let x = 0; x < w; x++) { + this.cells[y][x] = []; // each cell holds its own list + } + } +} + +grid.prototype = { + // return the array at (x,y) + cell(x, y) { + return this.cells[y][x]; + }, + + // alias for cell + at(pos) { + return this.cell(pos.x, pos.y); + }, + + // add an entity into a cell + add(entity, pos) { + this.cell(pos.x, pos.y).push(entity); + entity.coord = pos.slice(); + }, + + // remove an entity from a cell + remove(entity, pos) { + const c = this.cell(pos.x, pos.y); + const i = c.indexOf(entity); + if (i !== -1) c.splice(i, 1); + }, + + // bounds check + inBounds(pos) { + return ( + pos.x >= 0 && pos.x < this.width && + pos.y >= 0 && pos.y < this.height + ); + }, + + // 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++) { + const list = this.cells[y][x]; + for (let entity of list) { + fn(entity, entity.coord); + } + } + } + }, + + // 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++) { + out += this.cells[y][x].length; + } + if (y !== this.height - 1) out += "\n"; + } + return out; + } +}; diff --git a/examples/chess/movement.cm b/examples/chess/movement.cm new file mode 100644 index 00000000..32ba6ead --- /dev/null +++ b/examples/chess/movement.cm @@ -0,0 +1,32 @@ +var MovementSystem = function(grid, rules) { + this.grid = grid; + this.rules = rules || {}; // expects { canMove: fn } + this.turn = 'white'; +} + +MovementSystem.prototype.tryMove = function (piece, to) { + if (piece.colour != this.turn) return false; + + // normalise ‘to’ into our hybrid coord + var dest = [to.x ?? t[0], + to.y ?? to[1]]; + + if (!this.grid.inBounds(dest)) return false; + if (!this.rules.canMove(piece, piece.coord, dest, this.grid)) return false; + + var victims = this.grid.at(dest); + if (victims.length && victims[0].colour == piece.colour) return false; + if (victims.length) victims[0].captured = true; + + this.grid.remove(piece, piece.coord); + this.grid.add (piece, dest); + + // grid.add() re-creates coord; re-add .x/.y fields: + piece.coord.x = dest.x; + piece.coord.y = dest.y; + + this.turn = (this.turn == 'white') ? 'black' : 'white'; + return true; +}; + +return { MovementSystem: MovementSystem }; diff --git a/examples/chess/pieces.cm b/examples/chess/pieces.cm new file mode 100644 index 00000000..3d72fb4c --- /dev/null +++ b/examples/chess/pieces.cm @@ -0,0 +1,29 @@ +/* pieces.js – simple data holders + starting layout */ +function Piece(kind, colour) { + this.kind = kind; // "pawn" etc. + this.colour = colour; // "white"/"black" + this.sprite = colour + '_' + kind; // for draw2d.image + this.captured = false; + this.coord = [0,0]; +} +Piece.prototype.toString = function () { + return this.colour.charAt(0) + this.kind.charAt(0).toUpperCase(); +}; + +function startingPosition(grid) { + var W = 'white', B = 'black', x; + + // pawns + for (x = 0; x < 8; x++) { + grid.add(new Piece('pawn', W), [x, 6]); + grid.add(new Piece('pawn', B), [x, 1]); + } + // major pieces + var back = ['rook','knight','bishop','queen','king','bishop','knight','rook']; + for (x = 0; x < 8; x++) { + grid.add(new Piece(back[x], W), [x, 7]); + grid.add(new Piece(back[x], B), [x, 0]); + } +} + +return { Piece, startingPosition }; diff --git a/examples/chess/prosperon b/examples/chess/prosperon new file mode 100755 index 00000000..bbf99260 Binary files /dev/null and b/examples/chess/prosperon differ diff --git a/examples/chess/rules.cm b/examples/chess/rules.cm new file mode 100644 index 00000000..d14d5fcd --- /dev/null +++ b/examples/chess/rules.cm @@ -0,0 +1,45 @@ +/* helper – robust coord access */ +function cx(c) { return c.x ?? c[0] } +function cy(c) { return c.y ?? c[1] } + +/* simple move-shape checks */ +var deltas = { + pawn: function (pc, dx, dy, grid, to) { + var dir = (pc.colour == 'white') ? -1 : 1; + var base = (pc.colour == 'white') ? 6 : 1; + var one = (dy == dir && dx == 0 && grid.at(to).length == 0); + var two = (dy == 2 * dir && dx == 0 && cy(pc.coord) == base && + grid.at({ x: cx(pc.coord), y: cy(pc.coord)+dir }).length == 0 && + grid.at(to).length == 0); + var cap = (dy == dir && Math.abs(dx) == 1 && grid.at(to).length); + return one || two || cap; + }, + rook : function (pc, dx, dy) { return (dx == 0 || dy == 0); }, + bishop: function (pc, dx, dy) { return Math.abs(dx) == Math.abs(dy); }, + queen : function (pc, dx, dy) { return (dx == 0 || dy == 0 || Math.abs(dx) == Math.abs(dy)); }, + knight: function (pc, dx, dy) { return (Math.abs(dx) == 1 && Math.abs(dy) == 2) || + (Math.abs(dx) == 2 && Math.abs(dy) == 1); }, + king : function (pc, dx, dy) { return Math.max(Math.abs(dx), Math.abs(dy)) == 1; } +}; + +function clearLine(from, to, grid) { + var dx = Math.sign(cx(to) - cx(from)); + var dy = Math.sign(cy(to) - cy(from)); + var x = cx(from) + dx, y = cy(from) + dy; + while (x != cx(to) || y != cy(to)) { + if (grid.at({ x: x, y: y }).length) return false; + x += dx; y += dy; + } + return true; +} + +function canMove(piece, from, to, grid) { + var dx = cx(to) - cx(from); + var dy = cy(to) - cy(from); + var f = deltas[piece.kind]; + if (!f || !f(piece, dx, dy, grid, to)) return false; + if (piece.kind == 'knight') return true; + return clearLine(from, to, grid); +} + +return { canMove }; diff --git a/examples/chess/white_bishop.png b/examples/chess/white_bishop.png new file mode 100644 index 00000000..a75a9af7 Binary files /dev/null and b/examples/chess/white_bishop.png differ diff --git a/examples/chess/white_king.png b/examples/chess/white_king.png new file mode 100644 index 00000000..8da73d0c Binary files /dev/null and b/examples/chess/white_king.png differ diff --git a/examples/chess/white_knight.png b/examples/chess/white_knight.png new file mode 100644 index 00000000..5da9551c Binary files /dev/null and b/examples/chess/white_knight.png differ diff --git a/examples/chess/white_pawn.png b/examples/chess/white_pawn.png new file mode 100644 index 00000000..c34cce53 Binary files /dev/null and b/examples/chess/white_pawn.png differ diff --git a/examples/chess/white_queen.png b/examples/chess/white_queen.png new file mode 100644 index 00000000..90f26672 Binary files /dev/null and b/examples/chess/white_queen.png differ diff --git a/examples/chess/white_rook.png b/examples/chess/white_rook.png new file mode 100644 index 00000000..8d43fad2 Binary files /dev/null and b/examples/chess/white_rook.png differ diff --git a/examples/pong/config.cm b/examples/pong/config.cm new file mode 100644 index 00000000..7c612689 --- /dev/null +++ b/examples/pong/config.cm @@ -0,0 +1,5 @@ +return { + title: "Pong", + width: 858, + height: 525 +} diff --git a/examples/pong/main.ce b/examples/pong/main.ce new file mode 100644 index 00000000..be1958c5 --- /dev/null +++ b/examples/pong/main.ce @@ -0,0 +1,86 @@ +// main.js +var draw = use('draw2d') +var input = use('controller') +var config = use('config') +var color = use('color') + +prosperon.camera.transform.pos = [0,0] + +var paddleW = 10, paddleH = 80 +var p1 = {x: 30, y: config.height*0.5, speed: 300} +var p2 = {x: config.width-30, y: config.height*0.5, speed: 300} +var ball = {x: 0, y: 0, vx: 220, vy: 150, size: 10} +var score1 = 0, score2 = 0 + +function resetBall() { + ball.x = config.width*0.5 + ball.y = config.height*0.5 + // give it a random vertical bounce + ball.vy = (Math.random()<0.5 ? -1:1)*150 + // keep horizontal speed to the same magnitude + ball.vx = ball.vx>0 ? 220 : -220 +} + +resetBall() + +this.update = function(dt) { + // Move paddles: positive Y is up, so W/↑ means p.y += speed + if (input.keyboard.down('w')) p1.y += p1.speed*dt + if (input.keyboard.down('s')) p1.y -= p1.speed*dt + + // Paddle 2 movement (ArrowUp = up, ArrowDown = down) + if (input.keyboard.down('i')) p2.y += p2.speed*dt + if (input.keyboard.down('k')) p2.y -= p2.speed*dt + + // Clamp paddles to screen + if (p1.y < paddleH*0.5) p1.y = paddleH*0.5 + if (p1.y > config.height - paddleH*0.5) p1.y = config.height - paddleH*0.5 + if (p2.y < paddleH*0.5) p2.y = paddleH*0.5 + if (p2.y > config.height - paddleH*0.5) p2.y = config.height - paddleH*0.5 + + // Move ball + ball.x += ball.vx*dt + ball.y += ball.vy*dt + + // Bounce top/bottom + if (ball.y+ball.size*0.5>config.height || ball.y-ball.size*0.5<0) ball.vy = -ball.vy + + // Check paddle collisions + // p1 bounding box + var left1 = p1.x - paddleW*0.5, right1 = p1.x + paddleW*0.5 + var top1 = p1.y + paddleH*0.5, bottom1 = p1.y - paddleH*0.5 + // p2 bounding box + var left2 = p2.x - paddleW*0.5, right2 = p2.x + paddleW*0.5 + var top2 = p2.y + paddleH*0.5, bottom2 = p2.y - paddleH*0.5 + + // ball half-edges + var l = ball.x - ball.size*0.5, r = ball.x + ball.size*0.5 + var b = ball.y - ball.size*0.5, t = ball.y + ball.size*0.5 + + // Collide with paddle 1? + if (r>left1 && lbottom1 && bleft2 && lbottom2 && bconfig.width) { score1++; resetBall() } +} + +this.hud = function() { + // Clear screen black + draw.rectangle({x:0, y:0, width:config.width, height:config.height}, [0,0,0,1]) + + // Draw paddles + draw.rectangle({x:p1.x - paddleW*0.5, y:p1.y - paddleH*0.5, width:paddleW, height:paddleH}, color.white) + draw.rectangle({x:p2.x - paddleW*0.5, y:p2.y - paddleH*0.5, width:paddleW, height:paddleH}, color.white) + + // Draw ball + draw.rectangle({x:ball.x - ball.size*0.5, y:ball.y - ball.size*0.5, width:ball.size, height:ball.size}, color.white) + + // Simple score display + var msg = score1 + " " + score2 + draw.text(msg, {x:0, y:10, width:config.width, height:40}, null, 0, color.white, 0) +} diff --git a/examples/snake/config.cm b/examples/snake/config.cm new file mode 100644 index 00000000..79dbdf2a --- /dev/null +++ b/examples/snake/config.cm @@ -0,0 +1,5 @@ +return { + title: "Snake", + width: 600, + height: 600 +} diff --git a/examples/snake/main.ce b/examples/snake/main.ce new file mode 100644 index 00000000..36e979a4 --- /dev/null +++ b/examples/snake/main.ce @@ -0,0 +1,119 @@ +// main.js +var draw = use('draw2d') +var render = use('render') +var graphics = use('graphics') +var input = use('input') +var config = use('config') +var color = use('color') + +prosperon.camera.transform.pos = [0,0] + +var cellSize = 20 +var gridW = Math.floor(config.width / cellSize) +var gridH = Math.floor(config.height / cellSize) + +var snake, direction, nextDirection, apple +var moveInterval = 0.1 +var moveTimer = 0 +var gameState = "playing" + +function resetGame() { + var cx = Math.floor(gridW / 2) + var cy = Math.floor(gridH / 2) + snake = [ + {x: cx, y: cy}, + {x: cx-1, y: cy}, + {x: cx-2, y: cy} + ] + direction = {x:1, y:0} + nextDirection = {x:1, y:0} + spawnApple() + gameState = "playing" + moveTimer = 0 +} + +function spawnApple() { + apple = {x:Math.floor(Math.random()*gridW), y:Math.floor(Math.random()*gridH)} + // Re-spawn if apple lands on snake + for (var i=0; i= gridW) pos.x = 0 + if (pos.y < 0) pos.y = gridH - 1 + if (pos.y >= gridH) pos.y = 0 +} + +resetGame() + +this.update = function(dt) { + if (gameState != "playing") return + moveTimer += dt + if (moveTimer < moveInterval) return + moveTimer -= moveInterval + + // Update direction + direction = {x: nextDirection.x, y: nextDirection.y} + + // New head + var head = {x: snake[0].x + direction.x, y: snake[0].y + direction.y} + wrap(head) + + // Check collision with body + for (var i=0; i= 10) { + unlock_achievement(ACHIEVEMENTS.PLAY_10_GAMES); + } + + if (score >= 1000) { + unlock_achievement(ACHIEVEMENTS.HIGH_SCORE); + } +} + +// Cloud save example +function save_to_cloud(save_data) { + if (!steam_available) return false; + + var json_data = JSON.stringify(save_data); + return steam.cloud.cloud_write("savegame.json", json_data); +} + +function load_from_cloud() { + if (!steam_available) return null; + + var data = steam.cloud.cloud_read("savegame.json"); + if (data) { + // Convert ArrayBuffer to string + var decoder = new TextDecoder(); + var json_str = decoder.decode(data); + return JSON.parse(json_str); + } + + return null; +} + +// Cleanup +function cleanup_steam() { + if (steam_available) { + steam.steam_shutdown(); + log.console("Steam shut down"); + } +} + +// Export the API +module.exports = { + init: init_steam, + update: update_steam, + cleanup: cleanup_steam, + unlock_achievement: unlock_achievement, + update_stat: update_stat, + get_stat: get_stat, + start_game: start_game, + end_game: end_game, + save_to_cloud: save_to_cloud, + load_from_cloud: load_from_cloud, + is_available: function() { return steam_available; } +}; \ No newline at end of file diff --git a/examples/tetris/config.cm b/examples/tetris/config.cm new file mode 100644 index 00000000..47fc06c6 --- /dev/null +++ b/examples/tetris/config.cm @@ -0,0 +1,5 @@ +return { + title: "Tetris", + width:160, + height:144 +} diff --git a/examples/tetris/main.ce b/examples/tetris/main.ce new file mode 100644 index 00000000..a3423030 --- /dev/null +++ b/examples/tetris/main.ce @@ -0,0 +1,271 @@ +var draw = use('draw2d') +var input = use('input') +var config = use('config') +var color = use('color') + +prosperon.camera.transform.pos = [0,0] + +// Board constants +var COLS = 10, ROWS = 20 +var TILE = 6 // each cell is 6x6 + +// Board storage (2D), each cell is either 0 or a [r,g,b,a] color +var board = [] + +// Gravity timing +var baseGravity = 0.8 // seconds between drops at level 0 +var gravityTimer = 0 + +// Current piece & position +var piece = null +var pieceX = 0 +var pieceY = 0 + +// Next piece +var nextPiece = null + +// Score/lines/level +var score = 0 +var linesCleared = 0 +var level = 0 + +// Rotation lock to prevent spinning with W +var rotateHeld = false +var gameOver = false + +// Horizontal movement gating +var hMoveTimer = 0 +var hDelay = 0.2 // delay before repeated moves begin +var hRepeat = 0.05 // time between repeated moves +var prevLeft = false +var prevRight = false + +// Tetrimino definitions +var SHAPES = { + I: { color:[0,1,1,1], blocks:[[0,0],[1,0],[2,0],[3,0]] }, + O: { color:[1,1,0,1], blocks:[[0,0],[1,0],[0,1],[1,1]] }, + T: { color:[1,0,1,1], blocks:[[0,0],[1,0],[2,0],[1,1]] }, + S: { color:[0,1,0,1], blocks:[[1,0],[2,0],[0,1],[1,1]] }, + Z: { color:[1,0,0,1], blocks:[[0,0],[1,0],[1,1],[2,1]] }, + J: { color:[0,0,1,1], blocks:[[0,0],[0,1],[1,1],[2,1]] }, + L: { color:[1,0.5,0,1], blocks:[[2,0],[0,1],[1,1],[2,1]] } +} +var shapeKeys = Object.keys(SHAPES) + +// Initialize board with empty (0) +function initBoard() { + board = [] + for (var r=0; r [b[0], b[1]]) + } +} + +function spawnPiece() { + piece = nextPiece || randomShape() + nextPiece = randomShape() + pieceX = 3 + pieceY = 0 + // Collision on spawn => game over + if (collides(pieceX, pieceY, piece.blocks)) gameOver = true +} + +function collides(px, py, blocks) { + for (var i=0; i=COLS || y<0 || y>=ROWS) return true + if (y>=0 && board[y][x]) return true + } + return false +} + +// Lock piece into board +function lockPiece() { + for (var i=0; i=0) board[y][x] = piece.color + } +} + +// Rotate 90° clockwise +function rotate(blocks) { + // (x,y) => (y,-x) + for (var i=0; i=0;) { + if (board[r].every(cell => cell)) { + lines++ + // remove row + board.splice(r,1) + // add empty row on top + var newRow = [] + for (var c=0; c 0 && !collides(pieceX+1, pieceY, piece.blocks)) pieceX++ + + // If neither A nor D is pressed, reset the timer so next press is immediate + if (!leftPressed && !rightPressed) { + hMoveTimer = 0 + } + + // Decrement horizontal timer + hMoveTimer -= dt + prevLeft = leftPressed + prevRight = rightPressed + // ======= End Horizontal Movement Gate ======= + + // Rotate with W (once per press, no spinning) + if (input.keyboard.down('w')) { + if (!rotateHeld) { + rotateHeld = true + var test = piece.blocks.map(b => [b[0], b[1]]) + rotate(test) + if (!collides(pieceX, pieceY, test)) piece.blocks = test + } + } else { + rotateHeld = false + } + + // Soft drop if S is held (accelerates gravity) + var fallSpeed = input.keyboard.down('s') ? 10 : 1 + + // Gravity + gravityTimer += dt * fallSpeed + var dropInterval = Math.max(0.1, baseGravity - level*0.05) + if (gravityTimer >= dropInterval) { + gravityTimer = 0 + if (!collides(pieceX, pieceY+1, piece.blocks)) { + pieceY++ + } else { + placePiece() + } + } + + // Hard drop if space is held + if (input.keyboard.down('space')) { +// hardDrop() + } +} + +this.hud = function() { + // Clear screen + draw.rectangle({x:0, y:0, width:config.width, height:config.height}, [0,0,0,1]) + + // Draw board + for (var r=0; r= 360) angle = 360 + if (n <= 1) return [] + var points = [] + angle = Math.deg2rad(angle) + var arclen = angle / n + for (var i = 0; i < n; i++) points.push(math.rotate([radius, 0], start + arclen * i)) + return points +} +geometry.arc[cell.DOC] = ` +:param radius: The distance from center to the arc points. +:param angle: The total angle (in degrees) over which points are generated, capped at 360. +:param n: Number of segments (if <=1, empty array is returned). +:param start: Starting angle (in degrees), default 0. +:return: An array of 2D points along the arc. +Generate an arc (or partial circle) of n points, each angle spread equally over 'angle' degrees from 'start'. +` + +geometry.circle.points = function (radius, n) { + if (n <= 1) return [] + return geometry.arc(radius, 360, n) +} +geometry.circle.points[cell.DOC] = ` +:param radius: The circle's radius. +:param n: Number of points around the circle. +:return: An array of 2D points equally spaced around a full 360-degree circle. +Shortcut for geometry.arc(radius, 360, n). +` + +geometry.corners2points = function (ll, ur) { + return [ll, ll.add([ur.x, 0]), ur, ll.add([0, ur.y])] +} +geometry.corners2points[cell.DOC] = ` +:param ll: Lower-left 2D coordinate. +:param ur: Upper-right 2D coordinate (relative offset in x,y). +:return: A four-point array of corners [ll, lower-right, upper-right, upper-left]. +Similar to box.points, but calculates differently. +` + +geometry.sortpointsccw = function (points) { + var cm = points2cm(points) + var cmpoints = points.map(function (x) { return x.sub(cm) }) + var ccw = cmpoints.sort(function (a, b) { + var aatan = Math.atan2(a.y, a.x) + var batan = Math.atan2(b.y, b.x) + return aatan - batan + }) + return ccw.map(function (x) { return x.add(cm) }) +} +geometry.sortpointsccw[cell.DOC] = ` +:param points: An array of 2D points. +:return: A new array of the same points, sorted counterclockwise around their centroid. +Sort an array of points in CCW order based on their angles from the centroid. +` + +function points2cm(pts) { + var x = 0 + var y = 0 + var n = pts.length + pts.forEach(function (p) { + x += p[0] + y += p[1] + }) + return [x / n, y / n] +} + +geometry.points2cm = function(points) { + var x = 0 + var y = 0 + var n = points.length + points.forEach(function (p) { + x += p[0] + y += p[1] + }) + return [x / n, y / n] +} +geometry.points2cm[cell.DOC] = ` +:param points: An array of 2D points. +:return: The centroid (average x,y) of the given points. +` + +geometry.rect_intersection[cell.DOC] = ` +:param a: The first rectangle as {x, y, w, h}. +:param b: The second rectangle as {x, y, w, h}. +:return: A rectangle that is the intersection of the two. May have zero width/height if no overlap. +Return the intersection of two rectangles. The result may be empty if no intersection. +` + +geometry.rect_intersects[cell.DOC] = ` +:param a: Rectangle {x,y,w,h}. +:param b: Rectangle {x,y,w,h}. +:return: A boolean indicating whether the two rectangles overlap. +` + +geometry.rect_expand[cell.DOC] = ` +:param a: Rectangle {x,y,w,h}. +:param b: Rectangle {x,y,w,h}. +:return: A new rectangle that covers the bounds of both input rectangles. +Merge or combine two rectangles, returning their bounding rectangle. +` + +geometry.rect_inside[cell.DOC] = ` +:param inner: A rectangle to test. +:param outer: A rectangle that may contain 'inner'. +:return: True if 'inner' is completely inside 'outer', otherwise false. +` + +geometry.rect_random[cell.DOC] = ` +:param rect: A rectangle {x,y,w,h}. +:return: A random point within the rectangle (uniform distribution). +` + +geometry.cwh2rect[cell.DOC] = ` +:param center: A 2D point [cx, cy]. +:param wh: A 2D size [width, height]. +:return: A rectangle {x, y, w, h} with x,y set to center and w,h set to the given size. +Helper: convert a center point and width/height vector to a rect object. +` + +geometry.rect_point_inside[cell.DOC] = ` +:param rect: A rectangle {x,y,w,h}. +:param point: A 2D point [px, py]. +:return: True if the point lies inside the rectangle, otherwise false. +` + +geometry.rect_pos[cell.DOC] = ` +:param rect: A rectangle {x,y,w,h}. +:return: A 2D vector [x,y] giving the rectangle's position. +` + +geometry.rect_move[cell.DOC] = ` +:param rect: A rectangle {x,y,w,h}. +:param offset: A 2D vector to add to the rectangle's position. +:return: A new rectangle with updated x,y offset. +` + +return geometry diff --git a/icons/moon.gif b/icons/moon.gif new file mode 100644 index 00000000..74722d4d Binary files /dev/null and b/icons/moon.gif differ diff --git a/icons/no_tex.gif b/icons/no_tex.gif new file mode 100644 index 00000000..3f6017a2 Binary files /dev/null and b/icons/no_tex.gif differ diff --git a/icons_dev/airplane.png b/icons_dev/airplane.png new file mode 100644 index 00000000..142a6a5d Binary files /dev/null and b/icons_dev/airplane.png differ diff --git a/icons_dev/ak47.png b/icons_dev/ak47.png new file mode 100644 index 00000000..8ccd4942 Binary files /dev/null and b/icons_dev/ak47.png differ diff --git a/icons_dev/amputation.png b/icons_dev/amputation.png new file mode 100644 index 00000000..fb535071 Binary files /dev/null and b/icons_dev/amputation.png differ diff --git a/icons_dev/ant.png b/icons_dev/ant.png new file mode 100644 index 00000000..065aa650 Binary files /dev/null and b/icons_dev/ant.png differ diff --git a/icons_dev/archer.png b/icons_dev/archer.png new file mode 100644 index 00000000..42dc9452 Binary files /dev/null and b/icons_dev/archer.png differ diff --git a/icons_dev/armadillo.png b/icons_dev/armadillo.png new file mode 100644 index 00000000..347c2abe Binary files /dev/null and b/icons_dev/armadillo.png differ diff --git a/icons_dev/atom.png b/icons_dev/atom.png new file mode 100644 index 00000000..0a831dc6 Binary files /dev/null and b/icons_dev/atom.png differ diff --git a/icons_dev/banana.png b/icons_dev/banana.png new file mode 100644 index 00000000..89def08e Binary files /dev/null and b/icons_dev/banana.png differ diff --git a/icons_dev/bank.png b/icons_dev/bank.png new file mode 100644 index 00000000..3b7769f6 Binary files /dev/null and b/icons_dev/bank.png differ diff --git a/icons_dev/banknote.png b/icons_dev/banknote.png new file mode 100644 index 00000000..9e39d9a9 Binary files /dev/null and b/icons_dev/banknote.png differ diff --git a/icons_dev/barn.png b/icons_dev/barn.png new file mode 100644 index 00000000..ba047b58 Binary files /dev/null and b/icons_dev/barn.png differ diff --git a/icons_dev/barrel.png b/icons_dev/barrel.png new file mode 100644 index 00000000..e46c314c Binary files /dev/null and b/icons_dev/barrel.png differ diff --git a/icons_dev/basket.png b/icons_dev/basket.png new file mode 100644 index 00000000..bd8f1f97 Binary files /dev/null and b/icons_dev/basket.png differ diff --git a/icons_dev/bat.png b/icons_dev/bat.png new file mode 100644 index 00000000..f3119bfe Binary files /dev/null and b/icons_dev/bat.png differ diff --git a/icons_dev/bed.png b/icons_dev/bed.png new file mode 100644 index 00000000..6eba70ce Binary files /dev/null and b/icons_dev/bed.png differ diff --git a/icons_dev/belt.png b/icons_dev/belt.png new file mode 100644 index 00000000..a6fb0585 Binary files /dev/null and b/icons_dev/belt.png differ diff --git a/icons_dev/boar.png b/icons_dev/boar.png new file mode 100644 index 00000000..1d7cec64 Binary files /dev/null and b/icons_dev/boar.png differ diff --git a/icons_dev/broom.png b/icons_dev/broom.png new file mode 100644 index 00000000..50f6c642 Binary files /dev/null and b/icons_dev/broom.png differ diff --git a/icons_dev/cabin.png b/icons_dev/cabin.png new file mode 100644 index 00000000..00e8b65a Binary files /dev/null and b/icons_dev/cabin.png differ diff --git a/icons_dev/card-10-clubs.png b/icons_dev/card-10-clubs.png new file mode 100644 index 00000000..61031a10 Binary files /dev/null and b/icons_dev/card-10-clubs.png differ diff --git a/icons_dev/card-10-diamonds.png b/icons_dev/card-10-diamonds.png new file mode 100644 index 00000000..d946fc8e Binary files /dev/null and b/icons_dev/card-10-diamonds.png differ diff --git a/icons_dev/card-10-hearts.png b/icons_dev/card-10-hearts.png new file mode 100644 index 00000000..14465169 Binary files /dev/null and b/icons_dev/card-10-hearts.png differ diff --git a/icons_dev/card-10-spades.png b/icons_dev/card-10-spades.png new file mode 100644 index 00000000..b91aee66 Binary files /dev/null and b/icons_dev/card-10-spades.png differ diff --git a/icons_dev/card-2-clubs.png b/icons_dev/card-2-clubs.png new file mode 100644 index 00000000..1538497c Binary files /dev/null and b/icons_dev/card-2-clubs.png differ diff --git a/icons_dev/card-2-diamonds.png b/icons_dev/card-2-diamonds.png new file mode 100644 index 00000000..081ae930 Binary files /dev/null and b/icons_dev/card-2-diamonds.png differ diff --git a/icons_dev/card-2-hearts.png b/icons_dev/card-2-hearts.png new file mode 100644 index 00000000..1ffa718a Binary files /dev/null and b/icons_dev/card-2-hearts.png differ diff --git a/icons_dev/card-2-spades.png b/icons_dev/card-2-spades.png new file mode 100644 index 00000000..9bf4997b Binary files /dev/null and b/icons_dev/card-2-spades.png differ diff --git a/icons_dev/card-3-clubs.png b/icons_dev/card-3-clubs.png new file mode 100644 index 00000000..9155b6a2 Binary files /dev/null and b/icons_dev/card-3-clubs.png differ diff --git a/icons_dev/card-3-diamonds.png b/icons_dev/card-3-diamonds.png new file mode 100644 index 00000000..c71a7af2 Binary files /dev/null and b/icons_dev/card-3-diamonds.png differ diff --git a/icons_dev/card-3-hearts.png b/icons_dev/card-3-hearts.png new file mode 100644 index 00000000..8c87673f Binary files /dev/null and b/icons_dev/card-3-hearts.png differ diff --git a/icons_dev/card-3-spades.png b/icons_dev/card-3-spades.png new file mode 100644 index 00000000..b5d8e0a7 Binary files /dev/null and b/icons_dev/card-3-spades.png differ diff --git a/icons_dev/card-4-clubs.png b/icons_dev/card-4-clubs.png new file mode 100644 index 00000000..784f50f6 Binary files /dev/null and b/icons_dev/card-4-clubs.png differ diff --git a/icons_dev/card-4-diamonds.png b/icons_dev/card-4-diamonds.png new file mode 100644 index 00000000..bb97f25c Binary files /dev/null and b/icons_dev/card-4-diamonds.png differ diff --git a/icons_dev/card-4-hearts.png b/icons_dev/card-4-hearts.png new file mode 100644 index 00000000..7d4d1310 Binary files /dev/null and b/icons_dev/card-4-hearts.png differ diff --git a/icons_dev/card-4-spades.png b/icons_dev/card-4-spades.png new file mode 100644 index 00000000..4ab8e44a Binary files /dev/null and b/icons_dev/card-4-spades.png differ diff --git a/icons_dev/card-5-clubs.png b/icons_dev/card-5-clubs.png new file mode 100644 index 00000000..c72884d2 Binary files /dev/null and b/icons_dev/card-5-clubs.png differ diff --git a/icons_dev/card-5-diamonds.png b/icons_dev/card-5-diamonds.png new file mode 100644 index 00000000..d48ea1eb Binary files /dev/null and b/icons_dev/card-5-diamonds.png differ diff --git a/icons_dev/card-5-hearts.png b/icons_dev/card-5-hearts.png new file mode 100644 index 00000000..e0946b78 Binary files /dev/null and b/icons_dev/card-5-hearts.png differ diff --git a/icons_dev/card-5-spades.png b/icons_dev/card-5-spades.png new file mode 100644 index 00000000..c1910296 Binary files /dev/null and b/icons_dev/card-5-spades.png differ diff --git a/icons_dev/card-6-clubs.png b/icons_dev/card-6-clubs.png new file mode 100644 index 00000000..9521bf54 Binary files /dev/null and b/icons_dev/card-6-clubs.png differ diff --git a/icons_dev/card-6-diamonds.png b/icons_dev/card-6-diamonds.png new file mode 100644 index 00000000..1d85be34 Binary files /dev/null and b/icons_dev/card-6-diamonds.png differ diff --git a/icons_dev/card-6-hearts.png b/icons_dev/card-6-hearts.png new file mode 100644 index 00000000..eae80608 Binary files /dev/null and b/icons_dev/card-6-hearts.png differ diff --git a/icons_dev/card-6-spades.png b/icons_dev/card-6-spades.png new file mode 100644 index 00000000..1c1da030 Binary files /dev/null and b/icons_dev/card-6-spades.png differ diff --git a/icons_dev/card-7-clubs.png b/icons_dev/card-7-clubs.png new file mode 100644 index 00000000..68b544b5 Binary files /dev/null and b/icons_dev/card-7-clubs.png differ diff --git a/icons_dev/card-7-diamonds.png b/icons_dev/card-7-diamonds.png new file mode 100644 index 00000000..78f3f3d8 Binary files /dev/null and b/icons_dev/card-7-diamonds.png differ diff --git a/icons_dev/card-7-hearts.png b/icons_dev/card-7-hearts.png new file mode 100644 index 00000000..699d174c Binary files /dev/null and b/icons_dev/card-7-hearts.png differ diff --git a/icons_dev/card-7-spades.png b/icons_dev/card-7-spades.png new file mode 100644 index 00000000..bbdfd1e7 Binary files /dev/null and b/icons_dev/card-7-spades.png differ diff --git a/icons_dev/card-8-clubs.png b/icons_dev/card-8-clubs.png new file mode 100644 index 00000000..1be65dca Binary files /dev/null and b/icons_dev/card-8-clubs.png differ diff --git a/icons_dev/card-8-diamonds.png b/icons_dev/card-8-diamonds.png new file mode 100644 index 00000000..80cb1c91 Binary files /dev/null and b/icons_dev/card-8-diamonds.png differ diff --git a/icons_dev/card-8-hearts.png b/icons_dev/card-8-hearts.png new file mode 100644 index 00000000..b8fb7d06 Binary files /dev/null and b/icons_dev/card-8-hearts.png differ diff --git a/icons_dev/card-8-spades.png b/icons_dev/card-8-spades.png new file mode 100644 index 00000000..300b44a2 Binary files /dev/null and b/icons_dev/card-8-spades.png differ diff --git a/icons_dev/card-9-clubs.png b/icons_dev/card-9-clubs.png new file mode 100644 index 00000000..c9f7c759 Binary files /dev/null and b/icons_dev/card-9-clubs.png differ diff --git a/icons_dev/card-9-diamonds.png b/icons_dev/card-9-diamonds.png new file mode 100644 index 00000000..8b0a2fcc Binary files /dev/null and b/icons_dev/card-9-diamonds.png differ diff --git a/icons_dev/card-9-hearts.png b/icons_dev/card-9-hearts.png new file mode 100644 index 00000000..7820ffdf Binary files /dev/null and b/icons_dev/card-9-hearts.png differ diff --git a/icons_dev/card-9-spades.png b/icons_dev/card-9-spades.png new file mode 100644 index 00000000..b4907772 Binary files /dev/null and b/icons_dev/card-9-spades.png differ diff --git a/icons_dev/card-ace-clubs.png b/icons_dev/card-ace-clubs.png new file mode 100644 index 00000000..00b58823 Binary files /dev/null and b/icons_dev/card-ace-clubs.png differ diff --git a/icons_dev/card-ace-diamonds.png b/icons_dev/card-ace-diamonds.png new file mode 100644 index 00000000..17b79b28 Binary files /dev/null and b/icons_dev/card-ace-diamonds.png differ diff --git a/icons_dev/card-ace-hearts.png b/icons_dev/card-ace-hearts.png new file mode 100644 index 00000000..5fceaf31 Binary files /dev/null and b/icons_dev/card-ace-hearts.png differ diff --git a/icons_dev/card-ace-spades.png b/icons_dev/card-ace-spades.png new file mode 100644 index 00000000..44da0228 Binary files /dev/null and b/icons_dev/card-ace-spades.png differ diff --git a/icons_dev/card-discard.png b/icons_dev/card-discard.png new file mode 100644 index 00000000..5d75f07b Binary files /dev/null and b/icons_dev/card-discard.png differ diff --git a/icons_dev/card-draw.png b/icons_dev/card-draw.png new file mode 100644 index 00000000..9d2200ea Binary files /dev/null and b/icons_dev/card-draw.png differ diff --git a/icons_dev/card-jack-clubs.png b/icons_dev/card-jack-clubs.png new file mode 100644 index 00000000..efb1840e Binary files /dev/null and b/icons_dev/card-jack-clubs.png differ diff --git a/icons_dev/card-jack-diamonds.png b/icons_dev/card-jack-diamonds.png new file mode 100644 index 00000000..3d012699 Binary files /dev/null and b/icons_dev/card-jack-diamonds.png differ diff --git a/icons_dev/card-jack-hearts.png b/icons_dev/card-jack-hearts.png new file mode 100644 index 00000000..e0a9004b Binary files /dev/null and b/icons_dev/card-jack-hearts.png differ diff --git a/icons_dev/card-jack-spades.png b/icons_dev/card-jack-spades.png new file mode 100644 index 00000000..98a825a0 Binary files /dev/null and b/icons_dev/card-jack-spades.png differ diff --git a/icons_dev/card-joker.png b/icons_dev/card-joker.png new file mode 100644 index 00000000..6c4a2e3d Binary files /dev/null and b/icons_dev/card-joker.png differ diff --git a/icons_dev/card-king-clubs.png b/icons_dev/card-king-clubs.png new file mode 100644 index 00000000..c1c317fd Binary files /dev/null and b/icons_dev/card-king-clubs.png differ diff --git a/icons_dev/card-king-diamonds.png b/icons_dev/card-king-diamonds.png new file mode 100644 index 00000000..df9b503a Binary files /dev/null and b/icons_dev/card-king-diamonds.png differ diff --git a/icons_dev/card-king-hearts.png b/icons_dev/card-king-hearts.png new file mode 100644 index 00000000..db39c34b Binary files /dev/null and b/icons_dev/card-king-hearts.png differ diff --git a/icons_dev/card-king-spades.png b/icons_dev/card-king-spades.png new file mode 100644 index 00000000..efd85c53 Binary files /dev/null and b/icons_dev/card-king-spades.png differ diff --git a/icons_dev/card-pick.png b/icons_dev/card-pick.png new file mode 100644 index 00000000..99787932 Binary files /dev/null and b/icons_dev/card-pick.png differ diff --git a/icons_dev/card-queen-clubs.png b/icons_dev/card-queen-clubs.png new file mode 100644 index 00000000..49ab3874 Binary files /dev/null and b/icons_dev/card-queen-clubs.png differ diff --git a/icons_dev/card-queen-diamonds.png b/icons_dev/card-queen-diamonds.png new file mode 100644 index 00000000..f059f6c7 Binary files /dev/null and b/icons_dev/card-queen-diamonds.png differ diff --git a/icons_dev/card-queen-hearts.png b/icons_dev/card-queen-hearts.png new file mode 100644 index 00000000..57606fb6 Binary files /dev/null and b/icons_dev/card-queen-hearts.png differ diff --git a/icons_dev/card-queen-spades.png b/icons_dev/card-queen-spades.png new file mode 100644 index 00000000..3d283caf Binary files /dev/null and b/icons_dev/card-queen-spades.png differ diff --git a/icons_dev/card-random.png b/icons_dev/card-random.png new file mode 100644 index 00000000..455e748e Binary files /dev/null and b/icons_dev/card-random.png differ diff --git a/icons_dev/chess-bishop.png b/icons_dev/chess-bishop.png new file mode 100644 index 00000000..80dfc1d3 Binary files /dev/null and b/icons_dev/chess-bishop.png differ diff --git a/icons_dev/chess-king.png b/icons_dev/chess-king.png new file mode 100644 index 00000000..d020147e Binary files /dev/null and b/icons_dev/chess-king.png differ diff --git a/icons_dev/chess-knight.png b/icons_dev/chess-knight.png new file mode 100644 index 00000000..1001eafe Binary files /dev/null and b/icons_dev/chess-knight.png differ diff --git a/icons_dev/chess-pawn.png b/icons_dev/chess-pawn.png new file mode 100644 index 00000000..3740dccd Binary files /dev/null and b/icons_dev/chess-pawn.png differ diff --git a/icons_dev/chess-queen.png b/icons_dev/chess-queen.png new file mode 100644 index 00000000..2495c1c1 Binary files /dev/null and b/icons_dev/chess-queen.png differ diff --git a/icons_dev/chess-rook.png b/icons_dev/chess-rook.png new file mode 100644 index 00000000..c7759986 Binary files /dev/null and b/icons_dev/chess-rook.png differ diff --git a/icons_dev/chicken.png b/icons_dev/chicken.png new file mode 100644 index 00000000..e33d3965 Binary files /dev/null and b/icons_dev/chicken.png differ diff --git a/icons_dev/clarinet.png b/icons_dev/clarinet.png new file mode 100644 index 00000000..9aeb2e2b Binary files /dev/null and b/icons_dev/clarinet.png differ diff --git a/icons_dev/cloak.png b/icons_dev/cloak.png new file mode 100644 index 00000000..e388f975 Binary files /dev/null and b/icons_dev/cloak.png differ diff --git a/icons_dev/clown.png b/icons_dev/clown.png new file mode 100644 index 00000000..b4fdf970 Binary files /dev/null and b/icons_dev/clown.png differ diff --git a/icons_dev/coins.png b/icons_dev/coins.png new file mode 100644 index 00000000..778aaf01 Binary files /dev/null and b/icons_dev/coins.png differ diff --git a/icons_dev/compass.png b/icons_dev/compass.png new file mode 100644 index 00000000..c46542c2 Binary files /dev/null and b/icons_dev/compass.png differ diff --git a/icons_dev/cow.png b/icons_dev/cow.png new file mode 100644 index 00000000..4919bdfc Binary files /dev/null and b/icons_dev/cow.png differ diff --git a/icons_dev/crossed-swords.png b/icons_dev/crossed-swords.png new file mode 100644 index 00000000..d385b45c Binary files /dev/null and b/icons_dev/crossed-swords.png differ diff --git a/icons_dev/deer.png b/icons_dev/deer.png new file mode 100644 index 00000000..aff85482 Binary files /dev/null and b/icons_dev/deer.png differ diff --git a/icons_dev/dice.png b/icons_dev/dice.png new file mode 100644 index 00000000..e4bc09a6 Binary files /dev/null and b/icons_dev/dice.png differ diff --git a/icons_dev/dodo.png b/icons_dev/dodo.png new file mode 100644 index 00000000..7eed9618 Binary files /dev/null and b/icons_dev/dodo.png differ diff --git a/icons_dev/donkey.png b/icons_dev/donkey.png new file mode 100644 index 00000000..14a52043 Binary files /dev/null and b/icons_dev/donkey.png differ diff --git a/icons_dev/door.png b/icons_dev/door.png new file mode 100644 index 00000000..0b59c290 Binary files /dev/null and b/icons_dev/door.png differ diff --git a/icons_dev/duck.png b/icons_dev/duck.png new file mode 100644 index 00000000..fcf53c3d Binary files /dev/null and b/icons_dev/duck.png differ diff --git a/icons_dev/dynamite.png b/icons_dev/dynamite.png new file mode 100644 index 00000000..4aa2128d Binary files /dev/null and b/icons_dev/dynamite.png differ diff --git a/icons_dev/f-clef.png b/icons_dev/f-clef.png new file mode 100644 index 00000000..00c36556 Binary files /dev/null and b/icons_dev/f-clef.png differ diff --git a/icons_dev/fairy.png b/icons_dev/fairy.png new file mode 100644 index 00000000..792d5a9c Binary files /dev/null and b/icons_dev/fairy.png differ diff --git a/icons_dev/fangs.png b/icons_dev/fangs.png new file mode 100644 index 00000000..2ecad011 Binary files /dev/null and b/icons_dev/fangs.png differ diff --git a/icons_dev/fez.png b/icons_dev/fez.png new file mode 100644 index 00000000..8be35990 Binary files /dev/null and b/icons_dev/fez.png differ diff --git a/icons_dev/files.png b/icons_dev/files.png new file mode 100644 index 00000000..006ef97c Binary files /dev/null and b/icons_dev/files.png differ diff --git a/icons_dev/finch.png b/icons_dev/finch.png new file mode 100644 index 00000000..09ff4235 Binary files /dev/null and b/icons_dev/finch.png differ diff --git a/icons_dev/fire.png b/icons_dev/fire.png new file mode 100644 index 00000000..affd4d08 Binary files /dev/null and b/icons_dev/fire.png differ diff --git a/icons_dev/flashlight.png b/icons_dev/flashlight.png new file mode 100644 index 00000000..0de748fd Binary files /dev/null and b/icons_dev/flashlight.png differ diff --git a/icons_dev/footsteps.png b/icons_dev/footsteps.png new file mode 100644 index 00000000..6bf84ad6 Binary files /dev/null and b/icons_dev/footsteps.png differ diff --git a/icons_dev/forest.png b/icons_dev/forest.png new file mode 100644 index 00000000..f5afaad3 Binary files /dev/null and b/icons_dev/forest.png differ diff --git a/icons_dev/fox.png b/icons_dev/fox.png new file mode 100644 index 00000000..c3f85fb4 Binary files /dev/null and b/icons_dev/fox.png differ diff --git a/icons_dev/fuji.png b/icons_dev/fuji.png new file mode 100644 index 00000000..008837c2 Binary files /dev/null and b/icons_dev/fuji.png differ diff --git a/icons_dev/g-clef.png b/icons_dev/g-clef.png new file mode 100644 index 00000000..8c6fb2d5 Binary files /dev/null and b/icons_dev/g-clef.png differ diff --git a/icons_dev/gargoyle.png b/icons_dev/gargoyle.png new file mode 100644 index 00000000..78b85b76 Binary files /dev/null and b/icons_dev/gargoyle.png differ diff --git a/icons_dev/garlic.png b/icons_dev/garlic.png new file mode 100644 index 00000000..2ad3326a Binary files /dev/null and b/icons_dev/garlic.png differ diff --git a/icons_dev/gasmask.png b/icons_dev/gasmask.png new file mode 100644 index 00000000..c5c3e9be Binary files /dev/null and b/icons_dev/gasmask.png differ diff --git a/icons_dev/gate.png b/icons_dev/gate.png new file mode 100644 index 00000000..4ee1e886 Binary files /dev/null and b/icons_dev/gate.png differ diff --git a/icons_dev/goblin.png b/icons_dev/goblin.png new file mode 100644 index 00000000..b46c8f88 Binary files /dev/null and b/icons_dev/goblin.png differ diff --git a/icons_dev/goose.png b/icons_dev/goose.png new file mode 100644 index 00000000..0b8be833 Binary files /dev/null and b/icons_dev/goose.png differ diff --git a/icons_dev/gorilla.png b/icons_dev/gorilla.png new file mode 100644 index 00000000..48bea7db Binary files /dev/null and b/icons_dev/gorilla.png differ diff --git a/icons_dev/grass.png b/icons_dev/grass.png new file mode 100644 index 00000000..2e276aaa Binary files /dev/null and b/icons_dev/grass.png differ diff --git a/icons_dev/harp.png b/icons_dev/harp.png new file mode 100644 index 00000000..8880eb36 Binary files /dev/null and b/icons_dev/harp.png differ diff --git a/icons_dev/hatchet.png b/icons_dev/hatchet.png new file mode 100644 index 00000000..2f41dbb6 Binary files /dev/null and b/icons_dev/hatchet.png differ diff --git a/icons_dev/heart.png b/icons_dev/heart.png new file mode 100644 index 00000000..29adff6a Binary files /dev/null and b/icons_dev/heart.png differ diff --git a/icons_dev/hedgehog.png b/icons_dev/hedgehog.png new file mode 100644 index 00000000..878f46f6 Binary files /dev/null and b/icons_dev/hedgehog.png differ diff --git a/icons_dev/heron.png b/icons_dev/heron.png new file mode 100644 index 00000000..f107cf57 Binary files /dev/null and b/icons_dev/heron.png differ diff --git a/icons_dev/hook.png b/icons_dev/hook.png new file mode 100644 index 00000000..717828df Binary files /dev/null and b/icons_dev/hook.png differ diff --git a/icons_dev/hotdog.png b/icons_dev/hotdog.png new file mode 100644 index 00000000..d2433720 Binary files /dev/null and b/icons_dev/hotdog.png differ diff --git a/icons_dev/info.png b/icons_dev/info.png new file mode 100644 index 00000000..537eda38 Binary files /dev/null and b/icons_dev/info.png differ diff --git a/icons_dev/key.png b/icons_dev/key.png new file mode 100644 index 00000000..15dab45a Binary files /dev/null and b/icons_dev/key.png differ diff --git a/icons_dev/kite.png b/icons_dev/kite.png new file mode 100644 index 00000000..191ff530 Binary files /dev/null and b/icons_dev/kite.png differ diff --git a/icons_dev/ladder.png b/icons_dev/ladder.png new file mode 100644 index 00000000..0d1e2d5c Binary files /dev/null and b/icons_dev/ladder.png differ diff --git a/icons_dev/lambda.png b/icons_dev/lambda.png new file mode 100644 index 00000000..f376bafc Binary files /dev/null and b/icons_dev/lambda.png differ diff --git a/icons_dev/led.png b/icons_dev/led.png new file mode 100644 index 00000000..d246d1a7 Binary files /dev/null and b/icons_dev/led.png differ diff --git a/icons_dev/lemon.png b/icons_dev/lemon.png new file mode 100644 index 00000000..c7047d89 Binary files /dev/null and b/icons_dev/lemon.png differ diff --git a/icons_dev/log.png b/icons_dev/log.png new file mode 100644 index 00000000..2c2cb241 Binary files /dev/null and b/icons_dev/log.png differ diff --git a/icons_dev/maracas.png b/icons_dev/maracas.png new file mode 100644 index 00000000..2207bbcb Binary files /dev/null and b/icons_dev/maracas.png differ diff --git a/icons_dev/moai.png b/icons_dev/moai.png new file mode 100644 index 00000000..881ce7fe Binary files /dev/null and b/icons_dev/moai.png differ diff --git a/icons_dev/mole.png b/icons_dev/mole.png new file mode 100644 index 00000000..77b4174c Binary files /dev/null and b/icons_dev/mole.png differ diff --git a/icons_dev/mouse.png b/icons_dev/mouse.png new file mode 100644 index 00000000..2b82f86d Binary files /dev/null and b/icons_dev/mouse.png differ diff --git a/icons_dev/move.png b/icons_dev/move.png new file mode 100644 index 00000000..72b76eaa Binary files /dev/null and b/icons_dev/move.png differ diff --git a/icons_dev/necklace.png b/icons_dev/necklace.png new file mode 100644 index 00000000..adbacf50 Binary files /dev/null and b/icons_dev/necklace.png differ diff --git a/icons_dev/ocarina.png b/icons_dev/ocarina.png new file mode 100644 index 00000000..4aa6c58e Binary files /dev/null and b/icons_dev/ocarina.png differ diff --git a/icons_dev/panflute.png b/icons_dev/panflute.png new file mode 100644 index 00000000..624008ac Binary files /dev/null and b/icons_dev/panflute.png differ diff --git a/icons_dev/pangolin.png b/icons_dev/pangolin.png new file mode 100644 index 00000000..67e2e0bf Binary files /dev/null and b/icons_dev/pangolin.png differ diff --git a/icons_dev/pc.png b/icons_dev/pc.png new file mode 100644 index 00000000..fdd35f3e Binary files /dev/null and b/icons_dev/pc.png differ diff --git a/icons_dev/phone.png b/icons_dev/phone.png new file mode 100644 index 00000000..08c4bb41 Binary files /dev/null and b/icons_dev/phone.png differ diff --git a/icons_dev/piechart.png b/icons_dev/piechart.png new file mode 100644 index 00000000..eaacde05 Binary files /dev/null and b/icons_dev/piechart.png differ diff --git a/icons_dev/pin.png b/icons_dev/pin.png new file mode 100644 index 00000000..abf2597a Binary files /dev/null and b/icons_dev/pin.png differ diff --git a/icons_dev/pizza.png b/icons_dev/pizza.png new file mode 100644 index 00000000..f7b85a24 Binary files /dev/null and b/icons_dev/pizza.png differ diff --git a/icons_dev/poi.png b/icons_dev/poi.png new file mode 100644 index 00000000..77b617d0 Binary files /dev/null and b/icons_dev/poi.png differ diff --git a/icons_dev/polarbear.png b/icons_dev/polarbear.png new file mode 100644 index 00000000..36bce830 Binary files /dev/null and b/icons_dev/polarbear.png differ diff --git a/icons_dev/quiver.png b/icons_dev/quiver.png new file mode 100644 index 00000000..51e61134 Binary files /dev/null and b/icons_dev/quiver.png differ diff --git a/icons_dev/rabbit.png b/icons_dev/rabbit.png new file mode 100644 index 00000000..c5bf2071 Binary files /dev/null and b/icons_dev/rabbit.png differ diff --git a/icons_dev/raft.png b/icons_dev/raft.png new file mode 100644 index 00000000..d8001226 Binary files /dev/null and b/icons_dev/raft.png differ diff --git a/icons_dev/random.png b/icons_dev/random.png new file mode 100644 index 00000000..f97c30db Binary files /dev/null and b/icons_dev/random.png differ diff --git a/icons_dev/rat.png b/icons_dev/rat.png new file mode 100644 index 00000000..cfe089f8 Binary files /dev/null and b/icons_dev/rat.png differ diff --git a/icons_dev/rattlesnake.png b/icons_dev/rattlesnake.png new file mode 100644 index 00000000..6484d826 Binary files /dev/null and b/icons_dev/rattlesnake.png differ diff --git a/icons_dev/resize.png b/icons_dev/resize.png new file mode 100644 index 00000000..fcf82213 Binary files /dev/null and b/icons_dev/resize.png differ diff --git a/icons_dev/revolver.png b/icons_dev/revolver.png new file mode 100644 index 00000000..5f76c696 Binary files /dev/null and b/icons_dev/revolver.png differ diff --git a/icons_dev/ring.png b/icons_dev/ring.png new file mode 100644 index 00000000..5080052a Binary files /dev/null and b/icons_dev/ring.png differ diff --git a/icons_dev/rooster.png b/icons_dev/rooster.png new file mode 100644 index 00000000..12de22f4 Binary files /dev/null and b/icons_dev/rooster.png differ diff --git a/icons_dev/rss.png b/icons_dev/rss.png new file mode 100644 index 00000000..834f5f52 Binary files /dev/null and b/icons_dev/rss.png differ diff --git a/icons_dev/rupee.png b/icons_dev/rupee.png new file mode 100644 index 00000000..50e4d989 Binary files /dev/null and b/icons_dev/rupee.png differ diff --git a/icons_dev/sausage.png b/icons_dev/sausage.png new file mode 100644 index 00000000..9274310f Binary files /dev/null and b/icons_dev/sausage.png differ diff --git a/icons_dev/scorpion.png b/icons_dev/scorpion.png new file mode 100644 index 00000000..1cc01f4f Binary files /dev/null and b/icons_dev/scorpion.png differ diff --git a/icons_dev/screw.png b/icons_dev/screw.png new file mode 100644 index 00000000..51b4609c Binary files /dev/null and b/icons_dev/screw.png differ diff --git a/icons_dev/shamrock.png b/icons_dev/shamrock.png new file mode 100644 index 00000000..9637d5b6 Binary files /dev/null and b/icons_dev/shamrock.png differ diff --git a/icons_dev/sheep.png b/icons_dev/sheep.png new file mode 100644 index 00000000..ec731218 Binary files /dev/null and b/icons_dev/sheep.png differ diff --git a/icons_dev/shirt.png b/icons_dev/shirt.png new file mode 100644 index 00000000..b843762d Binary files /dev/null and b/icons_dev/shirt.png differ diff --git a/icons_dev/shop.png b/icons_dev/shop.png new file mode 100644 index 00000000..34176d39 Binary files /dev/null and b/icons_dev/shop.png differ diff --git a/icons_dev/shuriken.png b/icons_dev/shuriken.png new file mode 100644 index 00000000..8584dfea Binary files /dev/null and b/icons_dev/shuriken.png differ diff --git a/icons_dev/sloth.png b/icons_dev/sloth.png new file mode 100644 index 00000000..8d8e2f55 Binary files /dev/null and b/icons_dev/sloth.png differ diff --git a/icons_dev/snail.png b/icons_dev/snail.png new file mode 100644 index 00000000..cdc39bd2 Binary files /dev/null and b/icons_dev/snail.png differ diff --git a/icons_dev/snake.png b/icons_dev/snake.png new file mode 100644 index 00000000..5748e568 Binary files /dev/null and b/icons_dev/snake.png differ diff --git a/icons_dev/soap.png b/icons_dev/soap.png new file mode 100644 index 00000000..7624e482 Binary files /dev/null and b/icons_dev/soap.png differ diff --git a/icons_dev/sombrero.png b/icons_dev/sombrero.png new file mode 100644 index 00000000..8313cef6 Binary files /dev/null and b/icons_dev/sombrero.png differ diff --git a/icons_dev/stairs.png b/icons_dev/stairs.png new file mode 100644 index 00000000..13533663 Binary files /dev/null and b/icons_dev/stairs.png differ diff --git a/icons_dev/steak.png b/icons_dev/steak.png new file mode 100644 index 00000000..7a7132e8 Binary files /dev/null and b/icons_dev/steak.png differ diff --git a/icons_dev/tomato.png b/icons_dev/tomato.png new file mode 100644 index 00000000..c42758bd Binary files /dev/null and b/icons_dev/tomato.png differ diff --git a/icons_dev/trade.png b/icons_dev/trade.png new file mode 100644 index 00000000..55744976 Binary files /dev/null and b/icons_dev/trade.png differ diff --git a/icons_dev/trombone.png b/icons_dev/trombone.png new file mode 100644 index 00000000..17693bfc Binary files /dev/null and b/icons_dev/trombone.png differ diff --git a/icons_dev/trousers.png b/icons_dev/trousers.png new file mode 100644 index 00000000..9221e8e6 Binary files /dev/null and b/icons_dev/trousers.png differ diff --git a/icons_dev/trumpet.png b/icons_dev/trumpet.png new file mode 100644 index 00000000..41e8d192 Binary files /dev/null and b/icons_dev/trumpet.png differ diff --git a/icons_dev/tuba.png b/icons_dev/tuba.png new file mode 100644 index 00000000..ccbf3266 Binary files /dev/null and b/icons_dev/tuba.png differ diff --git a/icons_dev/tv.png b/icons_dev/tv.png new file mode 100644 index 00000000..2493476c Binary files /dev/null and b/icons_dev/tv.png differ diff --git a/icons_dev/ufo.png b/icons_dev/ufo.png new file mode 100644 index 00000000..67a320a5 Binary files /dev/null and b/icons_dev/ufo.png differ diff --git a/icons_dev/vial.png b/icons_dev/vial.png new file mode 100644 index 00000000..854bf92a Binary files /dev/null and b/icons_dev/vial.png differ diff --git a/icons_dev/watch.png b/icons_dev/watch.png new file mode 100644 index 00000000..84bd385b Binary files /dev/null and b/icons_dev/watch.png differ diff --git a/icons_dev/waterfall.png b/icons_dev/waterfall.png new file mode 100644 index 00000000..c155ecae Binary files /dev/null and b/icons_dev/waterfall.png differ diff --git a/icons_dev/well.png b/icons_dev/well.png new file mode 100644 index 00000000..815299bf Binary files /dev/null and b/icons_dev/well.png differ diff --git a/icons_dev/windmill.png b/icons_dev/windmill.png new file mode 100644 index 00000000..b8f3bd93 Binary files /dev/null and b/icons_dev/windmill.png differ diff --git a/icons_dev/wizard.png b/icons_dev/wizard.png new file mode 100644 index 00000000..635a5a4f Binary files /dev/null and b/icons_dev/wizard.png differ diff --git a/icons_dev/zipper.png b/icons_dev/zipper.png new file mode 100644 index 00000000..9c5308f1 Binary files /dev/null and b/icons_dev/zipper.png differ diff --git a/lcdsprite.cm b/lcdsprite.cm new file mode 100644 index 00000000..c0aa7083 --- /dev/null +++ b/lcdsprite.cm @@ -0,0 +1,107 @@ +var sprite = {} + +var graphics = use('graphics') +var render = use('render') +var draw2d = use('draw2d') +var sprite = use('sprite') + +var SPRITE = Symbol() /* raw C sprite */ +var POS = Symbol() /* cached JS copies of simple data */ +var ROT = Symbol() +var SCALE = Symbol() +var SKEW = Symbol() +var CENTER = Symbol() +var COLOR = Symbol() +var IMAGE = Symbol() + +var ursprite = { + get pos() { return this[POS] }, + set pos(v) { + this[POS] = v + this[SPRITE].pos = v + }, + + get center() { return this[CENTER] }, + set center(v){ + this[CENTER] = v + this[SPRITE].center = v + }, + + get rotation() { return this[ROT] }, + set rotation(t){ + this[ROT] = t + this[SPRITE].rotation = t * 2*Math.PI /* C expects radians */ + this[SPRITE].set_affine() + }, + + get scale() { return this[SCALE] }, + set scale(v){ + this[SCALE] = v + this[SPRITE].scale = v + this[SPRITE].set_affine() + }, + + get skew() { return this[SKEW] }, + set skew(v){ + this[SKEW] = v + this[SPRITE].skew = v + this[SPRITE].set_affine() + }, + + get layer() { return this[SPRITE].layer }, + set layer(n){ this[SPRITE].layer = n }, + + get color() { return this[COLOR] }, + set color(v){ + this[COLOR] = v + this[SPRITE].color = v + }, + + move(mv) { this[SPRITE].move(mv) }, + moveto(p){ + this.pos = p + }, + + get image() { return this[IMAGE] }, + set image(img) { + this[IMAGE] = img + this[SPRITE].set_image(img) + } +} + +var _sprites = [] + +var def_sprite = Object.freeze({ + pos: [0,0], + rotation: 0, + scale: [1,1], + center: [0,0], + color: [1,1,1,1], + skew: [0,0], + layer: 0, +}) + +sprite.create = function(image, info) { + info.__proto__ = def_sprite + var sp = Object.create(ursprite) + sp[SPRITE] = new sprite(info) + sp.image = graphics.texture(image) + + _sprites.push(sp) + return sp +} + +sprite.forEach = fn => { for (let s of _sprites) fn(s) } +sprite.values = () => _sprites.slice() +sprite.geometry= () => graphics.make_sprite_mesh(_sprites) + +var raws = [] +sprite.queue = function() { + if (raws.length != _sprites.length) + raws.length = _sprites.length + + for (var i = 0; i < _sprites.length; i++) raws[i] = _sprites[i] + return graphics.make_sprite_queue(_sprites.map(x => x[SPRITE])) +} + +return sprite diff --git a/nogame/config.js b/nogame/config.js new file mode 100644 index 00000000..1cc1d993 --- /dev/null +++ b/nogame/config.js @@ -0,0 +1,5 @@ +return { + title: "Prosperon [no game!]", + width:600, + height:600 +} diff --git a/nogame/main.js b/nogame/main.js new file mode 100644 index 00000000..5aed73c3 --- /dev/null +++ b/nogame/main.js @@ -0,0 +1,7 @@ +var clay = use("layout.js"); + +this.hud = function () { + clay.draw_commands(clay.draw([], _ => { + clay.text("No game yet! Make main.js to get started!"); + })); +}; diff --git a/prosperon.cm b/prosperon.cm new file mode 100644 index 00000000..ff102c3e --- /dev/null +++ b/prosperon.cm @@ -0,0 +1,1109 @@ +var prosperon = {} + +// This file is hard coded for the SDL GPU case + +var video = use('sdl_video') +var surface = use('surface') +var sdl_gpu = use('sdl_gpu') +var io = use('io') +var geometry = use('geometry') +var blob = use('blob') +var imgui = use('imgui') + +var os = use('os') + +var win_size = {width:500,height:500} + +function makeOrthoMetal(l,r,b,t,n,f){ + return [ + 2/(r-l), 0, 0, 0, + 0, 2/(t-b), 0, 0, + 0, 0, 1/(f-n), 0, + -(r+l)/(r-l), -(t+b)/(t-b), -n/(f-n), 1 + ] +} + +function make_camera_pblob(camera) { + def cw = camera.surface ? camera.surface.width : win_size.width; + def ch = camera.surface ? camera.surface.height : win_size.height; + + var world_w, world_h; + + if (camera.width && camera.aspect_ratio) { + // Use direct world dimensions if specified + world_w = camera.width; + world_h = camera.width / camera.aspect_ratio; + } else { + // Fallback to zoom-based calculation + def zoom = camera.zoom || ch; + world_h = zoom; + world_w = zoom * cw / ch; + } + + def l = camera.pos[0] - camera.anchor[0] * world_w; + def b = camera.pos[1] - camera.anchor[1] * world_h; + def r = l + world_w; + def t = b + world_h; + + def mat = makeOrthoMetal(l, r, b, t, 0, 1); + return geometry.array_blob(mat); +} + +var driver = "vulkan" +switch(os.platform()) { + case "Linux": + driver = "vulkan" + break + case "Windows": +// driver = "direct3d12" + driver = "vulkan" + break + case "macOS": + driver = "metal" + break +} + +var default_sampler = { + min_filter: "linear", + mag_filter: "linear", + mipmap: "nearest", + u: "repeat", + v: "repeat", + w: "repeat", + mip_bias: 0, + max_anisotropy: 0, + compare_op: "none", + min_lod: 0, + max_lod: 10, + anisotropy: false, + compare: false +}; + +var main_color = { + type:"2d", + format: "rgba8", + layers: 1, + mip_levels: 1, + samples: 0, + sampler:true, + color_target:true +}; + +var main_depth = { + type: "2d", + format: "d32 float s8", + layers:1, + mip_levels:1, + samples:0, + sampler:true, + depth_target:true +}; + +var default_window = { + // Basic properties + title: "Prosperon Window", + width: 640, + height: 360, + + // 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 (var 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 + +win_config.metal = true + +var window = new video.window(win_config) +prosperon.window = window +var win_proto = window.__proto__ +win_proto.toJSON = function() +{ + var flags = this.flags + var ret = { + title: this.title, + size: this.size, + pixel_size: this.sizeInPixels, + display_scale: this.displayScale, + pixel_density: this.pixelDensity, + pos: this.position, + opacity: this.opacity, + fullscreen: this.fullscreen, + safe_area: this.safe_area(), + } + + for (var i in flags) + ret[i] = flags[i] + return ret +} + +window.resizable = true + +var device = new sdl_gpu.gpu({ + shaders_msl:true, + shaders_metallib:true, + name: "metal" +}) +device.claim_window(window) +device.set_swapchain(window, 'sdr', 'vsync') + +var white_pixel = { + width:1, + height:1, + pixels: new blob(32, true), // 32 bits, all set to 1 for a white blob + pitch:32 +} + +stone(white_pixel.pixels) + +var shader_type = device.shader_format()[0] +shader_type = 'msl' + +var sampler_cache = {} + +function canonicalize_sampler(desc) { + return json.encode(desc) +} + +function get_sampler(desc) { + var key = canonicalize_sampler(desc) + + if (!sampler_cache[key]) { + + var sampler_config = json.decode(key) + sampler_cache[key] = new sdl_gpu.sampler(device, sampler_config) + } + + return sampler_cache[key] +} + +var std_sampler = get_sampler(true) + +// Shader and pipeline cache +var shader_cache = {} +var pipeline_cache = {} + +function upload(copypass, buffer, toblob) +{ + stone(toblob) + var trans = new sdl_gpu.transfer_buffer(device, { + size: toblob.length/8, + usage:"upload" + }) + + trans.copy_blob(device, toblob) + + copypass.upload_to_buffer({ + transfer_buffer: trans, + offset:0 + }, { + buffer: buffer, + offset: 0, + size: toblob.length/8 + }) +} + +function make_shader(sh_file) +{ + var file = `shaders/${shader_type}/${sh_file}.${shader_type}` + if (shader_cache[file]) return shader_cache[file] + var refl = json.decode(io.slurp(`shaders/reflection/${sh_file}.json`)) + + var shader = { + code: io.slurpbytes(file), + format: shader_type, + stage: sh_file.endsWith("vert") ? "vertex" : "fragment", + num_samplers: refl.separate_samplers ? refl.separate_samplers.length : 0, + num_textures: 0, + num_storage_buffers: refl.separate_storage_buffers ? refl.separate_storage_buffers.length : 0, + num_uniform_buffers: refl.ubos ? refl.ubos.length : 0, + entrypoint: shader_type == "msl" ? "main0" : "main" + } + + shader[GPU] = new sdl_gpu.shader(device, shader) + shader.reflection = refl; + shader_cache[file] = shader + shader.file = sh_file + return shader +} + +def material_pipeline_cache = {}; + +function get_pipeline_for_material(mat = {}) { + def key = json.encode({ + vert: mat.vertex || sprite_pipeline.vertex, + frag: mat.fragment || sprite_pipeline.fragment, + blend: mat.blend || sprite_pipeline.blend, + cull: mat.cull || sprite_pipeline.cull, + }); + + if (!material_pipeline_cache[key]) { + def cfg = Object.assign({}, sprite_pipeline, { + vertex: mat.vertex || sprite_pipeline.vertex, + fragment: mat.fragment || sprite_pipeline.fragment, + blend: mat.blend || sprite_pipeline.blend, + cull: mat.cull || sprite_pipeline.cull, + }); + + cfg.__proto__ = sprite_pipeline + material_pipeline_cache[key] = load_pipeline(cfg) + } + + return material_pipeline_cache[key]; +} + +function load_pipeline(config) +{ + // pull back the JS shader objects (they have `.reflection`) + def vertShader = make_shader(config.vertex); + def fragShader = make_shader(config.fragment); + + // build the GPU pipeline + def gpuPipeline = new sdl_gpu.graphics_pipeline(device, { + vertex: vertShader[GPU], + fragment: fragShader[GPU], + // ...all the other config fields... + primitive: config.primitive, + blend: config.blend, + cull: config.cull, + face: config.face, + depth: config.depth, + stencil: config.stencil, + alpha_to_coverage: config.alpha_to_coverage, + multisample: config.multisample, + label: config.label, + target: config.target, + vertex_buffer_descriptions: config.vertex_buffer_descriptions, + vertex_attributes: config.vertex_attributes + }); + + // stash the reflection in the JS wrapper for easy access later + gpuPipeline._reflection = { + vertex: vertShader.reflection, + fragment: fragShader.reflection + }; + + return gpuPipeline; +} + +// Helper function to pack JavaScript objects into binary blob for UBOs +function pack_ubo(obj, ubo_type, reflection) { + var type_def = reflection.types[ubo_type]; + if (!type_def) { + log.console(`Warning: No type definition found for ${ubo_type}`); + return geometry.array_blob([]); + } + + var result_blob = new blob(); + + // Process each member in the UBO structure + for (var member of type_def.members) { + var value = obj[member.name]; + + if (value == null) { + if (member.type == "vec4") { + result_blob.write_blob(geometry.array_blob([1, 1, 1, 1])); + } else if (member.type == "vec3") { + result_blob.write_blob(geometry.array_blob([1, 1, 1])); + } else if (member.type == "vec2") { + result_blob.write_blob(geometry.array_blob([1, 1])); + } else if (member.type == "float") { + result_blob.write_blob(geometry.array_blob([1])); + } + continue; + } + + // Convert value to appropriate format based on type + if (member.type == "vec4") { + if (Array.isArray(value)) { + result_blob.write_blob(geometry.array_blob(value)); + } else if (typeof value == "object" && value.r != null) { + // Color object + result_blob.write_blob(geometry.array_blob([value.r, value.g, value.b, value.a || 1])); + } else { + // Single value, expand to vec4 + result_blob.write_blob(geometry.array_blob([value, value, value, value])); + } + } else if (member.type == "vec3") { + if (Array.isArray(value)) { + result_blob.write_blob(geometry.array_blob(value)); + } else if (typeof value == 'object' && 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 (Array.isArray(value)) { + result_blob.write_blob(geometry.array_blob(value)); + } else { + result_blob.write_blob(geometry.array_blob([value, value])); + } + } else if (member.type == "float") { + result_blob.write_blob(geometry.array_blob([value])); + } + } + + return stone(result_blob) +} + +var imgui = use('imgui') +imgui.init(window, device) + +var io = use('io'); +var rasterize = use('rasterize'); +var time = use('time') +var tilemap = use('tilemap') + +var res = use('resources') +var input = use('input') + +var graphics = use('graphics') + +var camera = {} + +// Pipeline component definitions +var default_depth_state = { + compare: "always", // never/less/equal/less_equal/greater/not_equal/greater_equal/always + test: false, + write: false, + bias: 0, + bias_slope_scale: 0, + bias_clamp: 0 +} + +var default_stencil_state = { + compare: "always", // never/less/equal/less_equal/greater/neq/greq/always + fail: "keep", // keep/zero/replace/incr_clamp/decr_clamp/invert/incr_wrap/decr_wrap + depth_fail: "keep", + pass: "keep" +} + +var disabled_blend_state = { + enabled: false, + src_rgb: "zero", + dst_rgb: "zero", + op_rgb: "add", + src_alpha: "one", + dst_alpha: "zero", + op_alpha: "add" +} + +var alpha_blend_state = { + enabled: true, + src_rgb: "src_alpha", + dst_rgb: "one_minus_src_alpha", + op_rgb: "add", + src_alpha: "one", + dst_alpha: "one_minus_src_alpha", + op_alpha: "add" +} + +var default_multisample_state = { + count: 1, + mask: 0xFFFFFFFF, + domask: false +} + +// Helper function to create pipeline config +function create_pipeline_config(options) { + var config = { + vertex: options.vertex, + fragment: options.fragment, + primitive: options.primitive || "triangle", + fill: options.fill ?? true, + depth: options.depth || default_depth_state, + stencil: { + enabled: options.stencil_enabled ?? false, + front: options.stencil_front || default_stencil_state, + back: options.stencil_back || default_stencil_state, + test: options.stencil_test ?? false, + compare_mask: options.stencil_compare_mask ?? 0, + write_mask: options.stencil_write_mask ?? 0 + }, + blend: options.blend || disabled_blend_state, + cull: options.cull || "none", + face: options.face || "cw", + alpha_to_coverage: options.alpha_to_coverage ?? false, + multisample: options.multisample || default_multisample_state, + label: options.label || "pipeline", + target: options.target || {} + } + + // Ensure target has required properties + if (!config.target.color_targets) { + config.target.color_targets = [{ + format: "rgba8", + blend: config.blend + }] + } + + return config +} + +var gameactor + +var images = {} + +var renderer_commands = [] + +///// input ///// +var input_cb +var input_rate = 1/60 +function poll_input() { + var evs = input.get_events() + + // Filter and transform events + if (Array.isArray(evs)) { + var filteredEvents = [] +// var wantMouse = imgui.wantmouse() +// var wantKeys = imgui.wantkeys() + var wantMouse = false + var wantKeys = false + + 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) { + 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 + win_size.height + event.pos.y /= win_size.height + event.pos.x /= win_size.width + } + + filteredEvents.push(event) + } + } + + evs = filteredEvents + } + + input_cb(evs) + $_.delay(poll_input, input_rate) +} + +prosperon.input = function(fn) +{ + input_cb = fn + poll_input() +} + +var sprite_pipeline = { + vertex: "sprite.vert", + fragment: "sprite.frag", + cull: "none", + target: { + color_targets: [ + {format: device.swapchain_format(window), blend:alpha_blend_state} + ], + }, + vertex_buffer_descriptions: [ { slot:0, input_rate: "vertex", instance_step_rate: 0, + pitch: 8}, + {slot:1, input_rate:"vertex", instance_step_rate: 0, pitch: 8}, + {slot:2, input_rate:"vertex", instance_step_rate: 0, pitch: 16} + ], + vertex_attributes: [ + { location: 0, buffer_slot: 0, format: "float2", offset: 0}, + { location: 1, buffer_slot: 1, format: "float2", offset: 0}, + { location: 2, buffer_slot: 2, format: "float4", offset: 0} + ], + primitive: "triangle", + blend: alpha_blend_state +} + +var GPU = Symbol() + +var cur_cam +var cmd_fns = {} +cmd_fns.scissor = function(cmd) +{ + draw_queue.push(cmd) +} + +cmd_fns.camera = function(cmd) +{ + if (cmd.camera.surface && !cmd.camera.surface[GPU]) { + cmd.camera.surface[GPU] = new sdl_gpu.texture(device, cmd.camera.surface) + // Don't store sampler on texture - samplers belong to materials + } + draw_queue.push(cmd) +} + +var new_tex = [] + +function get_img_gpu(surface) +{ + if (!surface) return + + var full_mip = Math.floor(Math.log2(Math.max(surface.width,surface.height))) + 1 + var gpu = new sdl_gpu.texture(device, { + width: surface.width, + height: surface.height, + layers: 1, + mip_levels: full_mip, + samples: 0, + type: "2d", + format: "rgba8", + sampler: surface.sampler != null ? surface.sampler : default_sampler, + color_target: true + }) + + var tbuf = new sdl_gpu.transfer_buffer(device, { + size: surface.pixels.length/8, + usage: "upload" + }) + + tbuf.copy_blob(device, surface.pixels) + + copy_pass.upload_to_texture({ + transfer_buffer: tbuf, + offset: 0, + pixels_per_row: surface.width, + rows_per_layer: surface.height, + }, { + texture: gpu, + mip_level: 0, + layer: 0, + x: 0, y: 0, z: 0, + w: surface.width, + h: surface.height, + d: 1 + }, false); + + if (full_mip > 1) + new_tex.push(gpu) + + return gpu +} + +var pos_blob +var uv_blob +var color_blob +var index_blob + +var draw_queue = [] +var index_count = 0 +var vertex_count = 0 + +function render_geom(geom, img, pipeline = get_pipeline_for_material(null), material = null) +{ + if (!img[GPU]) { + if (img.surface) + img[GPU] = get_img_gpu(img.surface) + else + img[GPU] = get_img_gpu(img.cpu) + + if (!img[GPU]) return + } + + pos_blob.write_blob(geom.xy) + uv_blob.write_blob(geom.uv) + color_blob.write_blob(geom.color) + index_blob.write_blob(geom.indices) + + draw_queue.push({ + pipeline, + texture: img[GPU], + material: material, + num_indices: geom.num_indices, + first_index: index_count, + vertex_offset: vertex_count + }) + + vertex_count += (geom.xy.length/8) / 8 + index_count += geom.num_indices +} + +cmd_fns.draw_image = function(cmd) +{ + var img + if (typeof cmd.image == 'string') + img = graphics.texture(cmd.image) + else + img = cmd.image + + if (cmd.rect.width && !cmd.rect.height) + cmd.rect.height = cmd.rect.width * img.height / img.width + else if (cmd.rect.height && !cmd.rect.width) + cmd.rect.width = cmd.rect.height * img.width / img.height + else if (!cmd.rect.height && !cmd.rect.width) { + cmd.rect.width = img.width + cmd.rect.height = img.height + } + + var geom = geometry.make_rect_quad(cmd.rect) + geom.indices = geometry.make_quad_indices(1) + geom.num_indices = 6 + + // Ensure material has diffuse property for dynamic binding + if (!cmd.material) cmd.material = {} + if (!cmd.material.diffuse) cmd.material.diffuse = img + + var pipeline = get_pipeline_for_material(cmd.material) + render_geom(geom, img, pipeline, cmd.material) +} + +cmd_fns.draw_text = function(cmd) +{ + if (!cmd.text || !cmd.pos) return + + var font = graphics.get_font(cmd.font) + if (!font[GPU]) + font[GPU] = get_img_gpu(font.surface) + + var size = font.text_size(cmd.text, cmd.wrap, cmd.config.break, cmd.config.align) + cmd.pos.width ??= size[0] + cmd.pos.height ??= size[1] + + var mesh = font.make_text_buffer( + cmd.text, + cmd.pos, + [cmd.material.color.r, cmd.material.color.g, cmd.material.color.b, cmd.material.color.a], + cmd.wrap || 0, + cmd.config.break, + cmd.config.align + ) + + // Ensure material has diffuse property for dynamic binding + if (!cmd.material) cmd.material = {} + if (!cmd.material.diffuse) cmd.material.diffuse = font + + var pipeline = get_pipeline_for_material(cmd.material) + render_geom(mesh, font, pipeline, cmd.material) +} + +cmd_fns.tilemap = function(cmd) +{ + var geometryCommands = cmd.tilemap.draw() + + for (var geomCmd of geometryCommands) { + var img = graphics.texture(geomCmd.image) + if (!img) continue + + // Create a new material for each tile image with diffuse property + var tileMaterial = Object.assign({}, cmd.material || {}) + tileMaterial.diffuse = img + + var pipeline = get_pipeline_for_material(tileMaterial) + render_geom(geomCmd.geometry, img, pipeline, tileMaterial) + } +} + +cmd_fns.geometry = function(cmd) +{ + var img + if (typeof cmd.image == 'object') { + img = cmd.image + } else { + img = graphics.texture(cmd.image) + if (!img) return + } + + // Ensure material has diffuse property for dynamic binding + if (!cmd.material) cmd.material = {} + if (!cmd.material.diffuse) cmd.material.diffuse = img + + var pipeline = get_pipeline_for_material(cmd.material) + render_geom(cmd.geometry, img, pipeline, cmd.material) +} + +cmd_fns.draw_slice9 = function(cmd) +{ + var img = graphics.texture(cmd.image) + if (!img) return + + var slice_info = { + tile_top: true, + tile_bottom: true, + tile_left: true, + tile_right: true, + tile_center_x: true, + tile_center_y: true + } + + // Convert single slice value to LRTB object if needed + var slice_lrtb = cmd.slice + if (typeof cmd.slice == 'number') { + var slice_val = cmd.slice + if (slice_val > 0 && slice_val < 1) { + slice_lrtb = { + l: slice_val * img.width, + r: slice_val * img.width, + t: slice_val * img.height, + b: slice_val * img.height + } + } else { + slice_lrtb = { + l: slice_val, + r: slice_val, + t: slice_val, + b: slice_val + } + } + } else { + // Handle percentage values for each side individually + slice_lrtb = { + l: (cmd.slice.l > 0 && cmd.slice.l < 1) ? cmd.slice.l * img.width : cmd.slice.l, + r: (cmd.slice.r > 0 && cmd.slice.r < 1) ? cmd.slice.r * img.width : cmd.slice.r, + t: (cmd.slice.t > 0 && cmd.slice.t < 1) ? cmd.slice.t * img.height : cmd.slice.t, + b: (cmd.slice.b > 0 && cmd.slice.b < 1) ? cmd.slice.b * img.height : cmd.slice.b + } + } + + var mesh = geometry.slice9(img, cmd.rect, slice_lrtb, slice_info) + + // Ensure material has diffuse property for dynamic binding + if (!cmd.material) cmd.material = {} + if (!cmd.material.diffuse) cmd.material.diffuse = img + + var pipeline = get_pipeline_for_material(cmd.material) + render_geom(mesh, img, pipeline, cmd.material) +} + +cmd_fns.draw_rect = function(cmd) +{ + // Create geometry for a rectangle quad + var geom = geometry.make_rect_quad(cmd.rect, null, cmd.material.color) + geom.indices = geometry.make_quad_indices(1) + geom.num_indices = 6 + + // Use white_pixel as the texture so the color modulation works + if (!white_pixel[GPU]) + white_pixel[GPU] = get_img_gpu(white_pixel) + + var pipeline = get_pipeline_for_material(cmd.material) + render_geom(geom, {[GPU]: white_pixel[GPU]}, pipeline, cmd.material) +} + +var copy_pass + +prosperon.create_batch = function create_batch(draw_cmds, done) { + pos_blob = new blob + uv_blob = new blob + color_blob = new blob + index_blob = new blob + draw_queue = [] + index_count = 0 + vertex_count = 0 + new_tex = [] + + var render_queue = device.acquire_cmd_buffer() + copy_pass = render_queue.copy_pass() + + for (var cmd of draw_cmds) + if (cmd_fns[cmd.cmd]) + cmd_fns[cmd.cmd](cmd) + + var pos_buffer = new sdl_gpu.buffer(device,{ vertex:true, size:pos_blob.length/8}); + var uv_buffer = new sdl_gpu.buffer(device,{ vertex:true, size:uv_blob.length/8}); + var color_buffer = new sdl_gpu.buffer(device,{ vertex:true, size:color_blob.length/8}); + var index_buffer = new sdl_gpu.buffer(device,{ index:true, size:index_blob.length/8}); + + upload(copy_pass, pos_buffer, pos_blob) + upload(copy_pass, uv_buffer, uv_blob) + upload(copy_pass, color_buffer, color_blob) + upload(copy_pass, index_buffer, index_blob) + + copy_pass.end(); + + imgui.prepare(render_queue) + + for (var g of new_tex) + render_queue.generate_mipmaps(g) + + var render_pass + var render_target + + // State tracking for optimization + var current_pipeline = null + var current_camera_blob = null + var buffers_bound = false + + for (var cmd of draw_queue) { + if (cmd.cmd == "scissor") { + if (!cmd.rect) + render_pass.scissor({x:0,y:0,width:win_size.width,height:win_size.height}) + else + render_pass.scissor(cmd.rect) + + continue + } + if (cmd.camera) { + if (!cmd.camera.surface && render_target != "swap") { + if (render_pass) + render_pass.end() + render_target = "swap" + render_pass = render_queue.swapchain_pass(window) + // Reset state tracking when render pass changes + current_pipeline = null + buffers_bound = false + } else if (cmd.camera.surface && render_target != cmd.camera.surface) { + if (render_pass) + render_pass.end() + render_target = cmd.camera.surface + render_pass = render_queue.render_pass({ + color_targets: [{ + texture: cmd.camera.surface[GPU], + mip_level: 0, + layer: 0, + load: "clear", + clear_color: cmd.camera.background, + store: "store", + }] + }) + // Reset state tracking when render pass changes + current_pipeline = null + buffers_bound = false + } + + var vpW, vpH + + if (render_target == "swap") { + vpW = win_size.width + vpH = win_size.height + } else { + vpW = render_target.width + vpH = render_target.height + } + + render_pass.viewport({ + x: cmd.camera.viewport.x*vpW, + y: cmd.camera.viewport.y * vpH, + width: cmd.camera.viewport.width * vpW, + height: cmd.camera.viewport.height * vpH + }) + + var new_cam_blob = make_camera_pblob(cmd.camera) + // Only update camera uniform if it changed + if (current_camera_blob != new_cam_blob) { + render_queue.push_vertex_uniform_data(0, new_cam_blob) + current_camera_blob = new_cam_blob + } + continue + } + + // Only bind pipeline if it changed + if (current_pipeline != cmd.pipeline) { + render_pass.bind_pipeline(cmd.pipeline) + current_pipeline = cmd.pipeline + // When pipeline changes, we need to rebind buffers and uniforms + buffers_bound = false + current_camera_blob = null + } + + // Dynamic material binding - bind uniforms and textures from material + if (cmd.material && cmd.pipeline._reflection) { + def refl = cmd.pipeline._reflection; + + // Bind UBOs (uniform buffer objects) + if (refl.fragment && refl.fragment.ubos) { + for (def ubo of refl.fragment.ubos) { + def name = ubo.name; + def ubo_type = ubo.type; + + // For PSConstants or other UBOs, pack the material properties according to the UBO structure + def packed_blob = pack_ubo(cmd.material, ubo_type, refl.fragment); + + if (packed_blob && packed_blob.length > 0) { + // Push uniform data to both vertex and fragment stages +// render_queue.push_vertex_uniform_data(ubo.binding, packed_blob); + render_queue.push_fragment_uniform_data(ubo.binding, packed_blob); + } + } + } + + // Bind textures for any separate_images + if (refl.fragment && refl.fragment.separate_images) { + for (def imgDesc of refl.fragment.separate_images) { + def name = imgDesc.name; + def binding = imgDesc.binding; + def img = cmd.material[name]; + if (img) { + // Ensure texture is on GPU + if (!img[GPU]) { + if (img.surface) { + img[GPU] = get_img_gpu(img.surface); + } else if (img.cpu) { + img[GPU] = get_img_gpu(img.cpu); + } + } + + if (img[GPU]) { + // Use material's sampler or default_sampler + def sampler_desc = cmd.material.sampler || default_sampler; + render_pass.bind_samplers(false, binding, [{ + texture: img[GPU], + sampler: get_sampler(sampler_desc) + }]); + } + } + } + } + } + + // Only bind buffers if not already bound or pipeline changed + if (!buffers_bound) { + render_pass.bind_buffers(0, [ + { buffer: pos_buffer, offset: 0 }, + { buffer: uv_buffer, offset: 0 }, + { buffer: color_buffer, offset: 0 } + ]) + + render_pass.bind_index_buffer( + { buffer: index_buffer, offset: 0 }, // the binding itself is in bytes + 16 // 16 = Uint32 indices + ); + buffers_bound = true + } + + // Rebind camera uniform if needed after pipeline change + if (!current_camera_blob && cur_cam) { + render_queue.push_vertex_uniform_data(0, cur_cam) + current_camera_blob = cur_cam + } + + // Bind default texture if material didn't already bind "diffuse" + // Always bind the diffuse texture with material's sampler + if (cmd.texture) { + // Use material's sampler if specified, otherwise use default_sampler + var sampler_desc = (cmd.material && cmd.material.sampler) + ? cmd.material.sampler + : default_sampler + + var sampler_obj = get_sampler(sampler_desc) + render_pass.bind_samplers(false, 0, [{texture: cmd.texture, sampler: sampler_obj}]) + } + + render_pass.draw_indexed( + cmd.num_indices, + 1, + cmd.first_index, + cmd.vertex_offset, + 0 + ) + } + + imgui.endframe(render_queue, render_pass) + render_pass.end() + + render_queue.submit() + + if (done) done() +} + +////////// dmon hot reload //////// +function poll_file_changes() { + dmon.poll(e => { + log.console(json.encode(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) + } + } + } + }) + + $_.delay(poll_file_changes, 0.5) +} + +var dmon = use('dmon') +prosperon.dmon = function() +{ + if (!dmon) return + 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]) +} + +return prosperon diff --git a/rasterize.cm b/rasterize.cm new file mode 100644 index 00000000..ceaf0281 --- /dev/null +++ b/rasterize.cm @@ -0,0 +1,223 @@ +/** + * Rasterization module for converting shapes to pixels/rects + * Used for software rendering of complex shapes + */ + +var math = use('math') + +var rasterize = {} + +function within_wedge(dx, dy, start, end, full_circle) { + if (full_circle) return true + + var ang = Math.atan2(dy, dx) + if (ang < 0) ang += Math.PI * 2 + var t = ang / (Math.PI * 2) + + if (start <= end) return t >= start && t <= end + return t >= start || t <= end +} + +rasterize.ellipse = function ellipse(pos, radii, opt) { + opt = opt || {} + var rx = radii[0], ry = radii[1] + if (rx <= 0 || ry <= 0) return [] + + var cx = pos[0], cy = pos[1] + var raw_start = opt.start || 0 + var raw_end = opt.end || 1 + var full_circle = Math.abs(raw_end - raw_start) >= 1 - 1e-9 + var start = (raw_start % 1 + 1) % 1 + var end = (raw_end % 1 + 1) % 1 + var thickness = Math.max(1, opt.thickness || 1) + + var rx_i = rx - thickness, + ry_i = ry - thickness + var hole = (rx_i > 0 && ry_i > 0) + + if (!hole && thickness == 1) { + var points = [] + var rx_sq = rx * rx, ry_sq = ry * ry + var two_rx_sq = rx_sq << 1, two_ry_sq = ry_sq << 1 + var x = 0, y = ry, px = 0, py = two_rx_sq * y + var p = ry_sq - rx_sq * ry + 0.25 * rx_sq + + function add_pts(x, y) { + var pts = [ + [cx + x, cy + y], [cx - x, cy + y], + [cx + x, cy - y], [cx - x, cy - y] + ].filter(pt => within_wedge(pt[0]-cx, pt[1]-cy, start, end, full_circle)) + points = points.concat(pts) + } + + while (px < py) { + add_pts(x, y) + ++x; px += two_ry_sq + if (p < 0) p += ry_sq + px + else { --y; py -= two_rx_sq; p += ry_sq + px - py } + } + p = ry_sq*(x+.5)*(x+.5) + rx_sq*(y-1)*(y-1) - rx_sq*ry_sq + while (y >= 0) { + add_pts(x, y) + --y; py -= two_rx_sq + if (p > 0) p += rx_sq - py + else { ++x; px += two_ry_sq; p += rx_sq - py + px } + } + return {type: 'points', data: points} + } + + var strips = [] + var rx_sq = rx * rx, ry_sq = ry * ry + var rx_i_sq = rx_i * rx_i, ry_i_sq = ry_i * ry_i + + for (var dy = -ry; dy <= ry; ++dy) { + var yy = dy * dy + var x_out = Math.floor(rx * Math.sqrt(1 - yy / ry_sq)) + var y_screen = cy + dy + + var x_in = hole ? Math.floor(rx_i * Math.sqrt(1 - yy / ry_i_sq)) : -1 + + var run_start = null + for (var dx = -x_out; dx <= x_out; ++dx) { + if (hole && Math.abs(dx) <= x_in) { run_start = null; continue } + if (!within_wedge(dx, dy, start, end, full_circle)) { run_start = null; continue } + + if (run_start == null) run_start = cx + dx + + var last = (dx == x_out) + var next_in_ring = + !last && + !(hole && Math.abs(dx+1) <= x_in) && + within_wedge(dx+1, dy, start, end, full_circle) + + if (last || !next_in_ring) { + strips.push({ + x: run_start, + y: y_screen, + width: (cx + dx) - run_start + 1, + height: 1 + }) + run_start = null + } + } + } + return {type: 'rects', data: strips} +} + +rasterize.circle = function circle(pos, radius, opt) { + return rasterize.ellipse(pos, [radius, radius], opt) +} + +rasterize.outline_rect = function outline_rect(rect, thickness) { + if (thickness <= 0) { + return {type: 'rect', data: rect} + } + + if ((thickness << 1) >= rect.width || + (thickness << 1) >= rect.height) { + return {type: 'rect', data: rect} + } + + var x0 = rect.x, + y0 = rect.y, + x1 = rect.x + rect.width, + y1 = rect.y + rect.height + + return {type: 'rects', data: [ + { x:x0, y:y0, width:rect.width, height:thickness }, + { x:x0, y:y1-thickness, width:rect.width, height:thickness }, + { x:x0, y:y0+thickness, width:thickness, + height:rect.height - (thickness<<1) }, + { x:x1-thickness, y:y0+thickness, width:thickness, + height:rect.height - (thickness<<1) } + ]} +} + +rasterize.round_rect = function round_rect(rect, radius, thickness) { + thickness = thickness || 1 + + if (thickness <= 0) { + return rasterize.fill_round_rect(rect, radius) + } + + radius = Math.min(radius, rect.width >> 1, rect.height >> 1) + + if ((thickness << 1) >= rect.width || + (thickness << 1) >= rect.height || + thickness >= radius) { + return rasterize.fill_round_rect(rect, radius) + } + + var x0 = rect.x, + y0 = rect.y, + x1 = rect.x + rect.width - 1, + y1 = rect.y + rect.height - 1 + + var cx_l = x0 + radius, cx_r = x1 - radius + var cy_t = y0 + radius, cy_b = y1 - radius + var r_out = radius + var r_in = radius - thickness + + var rects = [ + { x:x0 + radius, y:y0, width:rect.width - (radius << 1), height:thickness }, + { x:x0 + radius, y:y1 - thickness + 1, width:rect.width - (radius << 1), height:thickness }, + { x:x0, y:y0 + radius, width:thickness, height:rect.height - (radius << 1) }, + { x:x1 - thickness + 1, y:y0 + radius, width:thickness, height:rect.height - (radius << 1) } + ] + + var strips = [] + + for (var dy = 0; dy < radius; ++dy) { + var dy_sq = dy * dy + var dx_out = Math.floor(Math.sqrt(r_out * r_out - dy_sq)) + var dx_in = (r_in > 0 && dy < r_in) + ? Math.floor(Math.sqrt(r_in * r_in - dy_sq)) + : -1 + var w = dx_out - dx_in + if (w <= 0) continue + + strips.push( + { x:cx_l - dx_out, y:cy_t - dy, width:w, height:1 }, + { x:cx_r + dx_in + 1, y:cy_t - dy, width:w, height:1 }, + { x:cx_l - dx_out, y:cy_b + dy, width:w, height:1 }, + { x:cx_r + dx_in + 1, y:cy_b + dy, width:w, height:1 } + ) + } + + return {type: 'rects', data: rects.concat(strips)} +} + +rasterize.fill_round_rect = function fill_round_rect(rect, radius) { + radius = Math.min(radius, rect.width >> 1, rect.height >> 1) + + var x0 = rect.x, + y0 = rect.y, + x1 = rect.x + rect.width - 1, + y1 = rect.y + rect.height - 1 + + var rects = [ + { x:x0 + radius, y:y0, width:rect.width - (radius << 1), height:rect.height }, + { x:x0, y:y0 + radius, width:radius, height:rect.height - (radius << 1) }, + { x:x1 - radius + 1, y:y0 + radius, width:radius, height:rect.height - (radius << 1) } + ] + + var cx_l = x0 + radius, cx_r = x1 - radius + var cy_t = y0 + radius, cy_b = y1 - radius + var caps = [] + + for (var dy = 0; dy < radius; ++dy) { + var dx = Math.floor(Math.sqrt(radius * radius - dy * dy)) + var w = (dx << 1) + 1 + + caps.push( + { x:cx_l - dx, y:cy_t - dy, width:w, height:1 }, + { x:cx_r - dx, y:cy_t - dy, width:w, height:1 }, + { x:cx_l - dx, y:cy_b + dy, width:w, height:1 }, + { x:cx_r - dx, y:cy_b + dy, width:w, height:1 } + ) + } + + return {type: 'rects', data: rects.concat(caps)} +} + +return rasterize \ No newline at end of file diff --git a/resources.cm b/resources.cm new file mode 100644 index 00000000..80eaa962 --- /dev/null +++ b/resources.cm @@ -0,0 +1,166 @@ +var io = use('io') + +Object.defineProperty(Function.prototype, "hashify", { + value: function () { + var hash = {} + var fn = this + function hashified(...args) { + var key = args[0] + if (hash[key] == null) hash[key] = fn(...args) + return hash[key] + } + return hashified + }, +}) + +// Merge of the old resources.js and packer.js functionalities +var Resources = {} + +// Recognized resource extensions +Resources.scripts = ["js"] +Resources.images = ["qoi", "png", "gif", "jpg", "jpeg", "ase", "aseprite"] +Resources.sounds = ["wav", "flac", "mp3", "qoa"] +Resources.fonts = ["ttf"] + +// Helper function: get extension from path in lowercase (e.g., "image.png" -> "png") +function getExtension(path) { + var idx = path.lastIndexOf('.') + if (idx < 0) return '' + return path.substring(idx + 1).toLowerCase() +} + +// Return true if ext is in at least one of the recognized lists +function isRecognizedExtension(ext) { + if (!ext) return false + if (Resources.scripts.includes(ext)) return true + if (Resources.images.includes(ext)) return true + if (Resources.sounds.includes(ext)) return true + if (Resources.fonts.includes(ext)) return true + if (Resources.lib.includes('.' + ext)) return true // for .so or .dll + return false +} + +function find_in_path(filename, exts = []) { + if (typeof filename != 'string') return null + + if (filename.includes('.')) { + var candidate = filename // possibly need "/" ? + if (io.exists(candidate) && !io.is_directory(candidate)) return candidate + return null + } + + // Only check extensions if exts is provided and not empty + if (exts.length > 0) { + for (var ext of exts) { + var candidate = filename + '.' + ext + if (io.exists(candidate) && !io.is_directory(candidate)) return candidate + } + } else { + // Fallback to extensionless file only if no extensions are specified + var candidate = filename + if (io.exists(candidate) && !io.is_directory(candidate)) return candidate + } + return null +} + +// Return a canonical path (the real directory plus the path) +Resources.canonical = function(file) { + return io.realdir(file) + file +} + +// The resource finders +Resources.find_image = function(file) { + return find_in_path(file, Resources.images) +}.hashify() + +Resources.find_sound = function(file) { + return find_in_path(file, Resources.sounds) +}.hashify() + +Resources.find_script = function(file) { + return find_in_path(file, Resources.scripts) +}.hashify() + +Resources.find_font = function(file) { + return find_in_path(file, Resources.fonts) +}.hashify() + +// .prosperonignore reading helper +function read_ignore(dir) { + var path = dir + '/.prosperonignore' + var patterns = [] + if (io.exists(path)) { + var lines = io.slurp(path).split('\n') + for (var line of lines) { + line = line.trim() + if (!line || line.startsWith('#')) continue + patterns.push(line) + } + } + return patterns +} + +// Return a list of recognized files in the directory (and subdirectories), +// skipping those matched by .prosperonignore. Directory paths are skipped. +Resources.getAllFiles = function(dir = "") { + var patterns = read_ignore(dir) + var all = io.globfs(patterns, dir) + var results = [] + for (var f of all) { + var fullPath = dir + '/' + f + try { + var st = io.stat(fullPath) + // skip directories (filesize=0) or unrecognized extension + if (!st.filesize) continue + var ext = getExtension(f) + if (!isRecognizedExtension(ext)) continue + results.push(fullPath) + } catch(e) {} + } + return results +} +Resources.getAllFiles[cell.DOC] = ` +Return a list of recognized files in the given directory that are not matched by +.prosperonignore, skipping directories. Recognized extensions include scripts, +images, sounds, fonts, and libs. + +:param dir: The directory to search. +:return: An array of recognized file paths. +` + +// Categorize files by resource type +Resources.gatherStats = function(filePaths) { + var stats = { + scripts:0, images:0, sounds:0, fonts:0, lib:0, other:0, total:filePaths.length + } + for (var path of filePaths) { + var ext = getExtension(path) + if (Resources.scripts.includes(ext)) { + stats.scripts++ + continue + } + if (Resources.images.includes(ext)) { + stats.images++ + continue + } + if (Resources.sounds.includes(ext)) { + stats.sounds++ + continue + } + if (Resources.fonts.includes(ext)) { + stats.fonts++ + continue + } + stats.other++ + } + return stats +} +Resources.gatherStats[cell.DOC] = ` +Analyze a list of recognized files and categorize them by scripts, images, sounds, +fonts, libs, or other. Return a stats object with these counts and the total. + +:param filePaths: An array of file paths to analyze. +:return: { scripts, images, sounds, fonts, lib, other, total } +` + +return Resources diff --git a/sdl_video.ce b/sdl_video.ce new file mode 100644 index 00000000..990001b3 --- /dev/null +++ b/sdl_video.ce @@ -0,0 +1,865 @@ +var video = use('sdl_video'); +var imgui = use('imgui'); + +// SDL Video Actor +// This actor runs on the main thread and handles all SDL video operations +var surface = use('surface'); +var input = use('input') + +var ren +var win + +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 config = Object.assign({}, default_window, arg[0] || {}); +win = new video.window(config); + +// Resource tracking +var resources = { + texture: {}, + surface: {}, + cursor: {} +}; + +// ID counter for resource allocation +var next_id = 1; + +// Helper to allocate new ID +function allocate_id() { + return next_id++; +} + +// Message handler +$_.receiver(function(msg) { + if (!msg.kind || !msg.op) { + send(msg, {error: "Message must have 'kind' and 'op' fields"}); + return; + } + + var response = {}; + +// log.console(json.encode(msg)) + + try { + switch (msg.kind) { + case 'window': + response = handle_window(msg); + break; + case 'renderer': + response = handle_renderer(msg); + break; + case 'texture': + response = handle_texture(msg); + break; + case 'surface': + response = handle_surface(msg); + break; + case 'cursor': + response = handle_cursor(msg); + break; + case 'mouse': + response = handle_mouse(msg); + break; + case 'keyboard': + response = handle_keyboard(msg); + break; + case 'imgui': + response = handle_imgui(msg); + break; + case 'input': + response = input.get_events(); + // Filter and transform events + if (ren && Array.isArray(response)) { + var filteredEvents = []; + var wantMouse = imgui.wantmouse(); + var wantKeys = imgui.wantkeys(); + + for (var i = 0; i < response.length; i++) { + var event = response[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 = ren.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 = ren.coordsFromWindow(event.pos); + event.pos = logicalPos; + } + + filteredEvents.push(event); + } + } + + response = filteredEvents; + } + break; + default: + response = {error: "Unknown kind: " + msg.kind}; + } + } catch (e) { + response = {error: e.toString()}; + log.error(e) + } + + send(msg, response); +}); + +// Window operations +function handle_window(msg) { + switch (msg.op) { + case 'destroy': + win.destroy(); + win = null + return {success: true}; + + case 'show': + win.visible = true; + return {success: true}; + + case 'hide': + win.visible = false; + return {success: true}; + + case 'get': + var prop = msg.data ? msg.data.property : null; + if (!prop) return {error: "Missing property name"}; + + // Handle special cases + if (prop == 'surface') { + var surf = win.surface; + if (!surf) return {data: null}; + var surf_id = allocate_id(); + resources.surface[surf_id] = surf; + return {data: surf_id}; + } + + return {data: win[prop]}; + + case 'set': + var prop = msg.data ? msg.data.property : null; + var value = msg.data ? msg.data.value : null; + if (!prop) return {error: "Missing property name"}; + + // Validate property is settable + var readonly = ['id', 'pixelDensity', 'displayScale', 'sizeInPixels', 'flags', 'surface']; + if (readonly.indexOf(prop) != -1) { + return {error: "Property '" + prop + "' is read-only"}; + } + + win[prop] = value; + return {success: true}; + + case 'fullscreen': + win.fullscreen(); + return {success: true}; + + case 'updateSurface': + win.updateSurface(); + return {success: true}; + + case 'updateSurfaceRects': + if (!msg.data || !msg.data.rects) return {error: "Missing rects array"}; + win.updateSurfaceRects(msg.data.rects); + return {success: true}; + + case 'raise': + win.raise(); + return {success: true}; + + case 'restore': + win.restore(); + return {success: true}; + + case 'flash': + win.flash(msg.data ? msg.data.operation : 'briefly'); + return {success: true}; + + case 'sync': + win.sync(); + return {success: true}; + + case 'setIcon': + if (!msg.data || !msg.data.surface_id) return {error: "Missing surface_id"}; + var surf = resources.surface[msg.data.surface_id]; + if (!surf) return {error: "Invalid surface id"}; + win.set_icon(surf); + return {success: true}; + + case 'makeRenderer': + log.console("MAKE RENDERER") + if (ren) + return {reason: "Already made a renderer"} + ren = win.make_renderer() + // Initialize ImGui with the window and renderer + imgui.init(win, ren); + imgui.newframe() + return {success:true}; + + default: + return {error: "Unknown window operation: " + msg.op}; + } +} + +// Renderer operation functions +var renderfuncs = { + destroy: function(msg) { + ren = null + return {success: true}; + }, + + clear: function(msg) { + ren.clear(); + return {success: true}; + }, + + present: function(msg) { + ren.present(); + return {success: true}; + }, + + flush: function(msg) { + ren.flush(); + return {success: true}; + }, + + get: function(msg) { + var prop = msg.data ? msg.data.property : null; + if (!prop) return {error: "Missing property name"}; + + // Handle special getters that might return objects + if (prop == 'drawColor') { + var color = ren[prop]; + if (color && typeof color == 'object') { + // Convert color object to array format [r,g,b,a] + return {data: [color.r || 0, color.g || 0, color.b || 0, color.a || 255]}; + } + } + + return {data: ren[prop]}; + }, + + set: function(msg) { + var prop = msg.prop + var value = msg.value + if (!prop) return {error: "Missing property name"}; + + if (!value) return {error: "No value to set"} + + // Validate property is settable + var readonly = ['window', 'name', 'outputSize', 'currentOutputSize', 'logicalPresentationRect', 'safeArea']; + if (readonly.indexOf(prop) != -1) { + return {error: "Property '" + prop + "' is read-only"}; + } + + // Special handling for render target + if (prop == 'target' && value != null && value != null) { + var tex = resources.texture[value]; + if (!tex) return {error: "Invalid texture id"}; + value = tex; + } + + ren[prop] = value; + return {success: true}; + }, + + line: function(msg) { + if (!msg.data || !msg.data.points) return {error: "Missing points array"}; + ren.line(msg.data.points); + return {success: true}; + }, + + point: function(msg) { + if (!msg.data || !msg.data.points) return {error: "Missing points"}; + ren.point(msg.data.points); + return {success: true}; + }, + + rect: function(msg) { + if (!msg.data || !msg.data.rect) return {error: "Missing rect"}; + ren.rect(msg.data.rect); + return {success: true}; + }, + + fillRect: function(msg) { + if (!msg.data || !msg.data.rect) return {error: "Missing rect"}; + ren.fillRect(msg.data.rect); + return {success: true}; + }, + + rects: function(msg) { + if (!msg.data || !msg.data.rects) return {error: "Missing rects"}; + ren.rects(msg.data.rects); + return {success: true}; + }, + + lineTo: function(msg) { + if (!msg.data || !msg.data.a || !msg.data.b) return {error: "Missing points a and b"}; + ren.lineTo(msg.data.a, msg.data.b); + return {success: true}; + }, + + texture: function(msg) { + if (!msg.data) return {error: "Missing texture data"}; + var tex_id = msg.data.texture_id; + if (!tex_id || !resources.texture[tex_id]) return {error: "Invalid texture id"}; + ren.texture( + resources.texture[tex_id], + msg.data.src, + msg.data.dst, + msg.data.angle || 0, + msg.data.anchor || {x:0.5, y:0.5} + ); + return {success: true}; + }, + + copyTexture: function(msg) { + if (!msg.data) return {error: "Missing texture data"}; + var tex_id = msg.data.texture_id; + if (!tex_id || !resources.texture[tex_id]) return {error: "Invalid texture id"}; + var tex = resources.texture[tex_id]; + + // Use the texture method with normalized coordinates + ren.texture( + tex, + msg.data.src || {x:0, y:0, width:tex.width, height:tex.height}, + msg.data.dest || {x:0, y:0, width:tex.width, height:tex.height}, + 0, // No rotation + {x:0, y:0} // Top-left anchor + ); + return {success: true}; + }, + + sprite: function(msg) { + if (!msg.data || !msg.data.sprite) return {error: "Missing sprite data"}; + ren.sprite(msg.data.sprite); + return {success: true}; + }, + + geometry: function(msg) { + if (!msg.data) return {error: "Missing geometry data"}; + var tex_id = msg.data.texture_id; + var tex = tex_id ? resources.texture[tex_id] : null; + ren.geometry(tex, msg.data.geometry); + return {success: true}; + }, + + geometry_raw: function geometry_raw(msg) { + var geom = msg.data + ren.geometry_raw(resources.texture[geom.texture_id], 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); + }, + + debugText: function(msg) { + if (!msg.data || !msg.data.text) return {error: "Missing text"}; + ren.debugText([msg.data.pos.x, msg.data.pos.y], msg.data.text); + return {success: true}; + }, + + clipEnabled: function(msg) { + return {data: ren.clipEnabled()}; + }, + + texture9Grid: function(msg) { + if (!msg.data) return {error: "Missing data"}; + var tex_id = msg.data.texture_id; + if (!tex_id || !resources.texture[tex_id]) return {error: "Invalid texture id"}; + ren.texture9Grid( + resources.texture[tex_id], + msg.data.src, + msg.data.leftWidth, + msg.data.rightWidth, + msg.data.topHeight, + msg.data.bottomHeight, + msg.data.scale, + msg.data.dst + ); + return {success: true}; + }, + + textureTiled: function(msg) { + if (!msg.data) return {error: "Missing data"}; + var tex_id = msg.data.texture_id; + if (!tex_id || !resources.texture[tex_id]) return {error: "Invalid texture id"}; + ren.textureTiled( + resources.texture[tex_id], + msg.data.src, + msg.data.scale || 1.0, + msg.data.dst + ); + return {success: true}; + }, + + readPixels: function(msg) { + var surf = ren.readPixels(msg.data ? msg.data.rect : null); + if (!surf) return {error: "Failed to read pixels"}; + var surf_id = allocate_id(); + resources.surface[surf_id] = surf; + return {id: surf_id}; + }, + + loadTexture: function(msg) { + if (!msg.data) throw new Error("Missing data") + + var tex; + // Direct surface data + var surf = new surface(msg.data) + + if (!surf) + throw new Error("Must provide surface_id or surface data") + + tex = ren.load_texture(surf); + + if (!tex) throw new Error("Failed to load texture") + + // 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, + }; + }, + + coordsFromWindow: function(msg) { + if (!msg.data || !msg.data.pos) return {error: "Missing pos"}; + return {data: ren.coordsFromWindow(msg.data.pos)}; + }, + + coordsToWindow: function(msg) { + if (!msg.data || !msg.data.pos) return {error: "Missing pos"}; + return {data: ren.coordsToWindow(msg.data.pos)}; + }, + + batch: function(msg) { + if (!msg.data || !Array.isArray(msg.data)) return {error: "Missing or invalid data array"}; + + for (var i = 0; i < msg.data.length; i++) + handle_renderer(msg.data[i]); + + return {success:true}; + }, + + imgui_render: function(msg) { + imgui.endframe(ren); + imgui.newframe() + return {success: true}; + } +}; + +// Renderer operations +function handle_renderer(msg) { + if (!ren) return{reason:'no renderer!'} + + var func = renderfuncs[msg.op]; + if (func) { + return func(msg); + } else { + return {error: "Unknown renderer operation: " + msg.op}; + } +} + +// Texture operations +function handle_texture(msg) { + // Special case: create needs a renderer + if (msg.op == 'create') { + if (!msg.data) return {error: "Missing texture data"}; + var ren_id = msg.data.renderer_id; + if (!ren_id || !resources.renderer[ren_id]) return {error: "Invalid renderer id"}; + + var tex; + var renderer = resources.renderer[ren_id]; + + // Create from surface + if (msg.data.surface_id) { + var surf = resources.surface[msg.data.surface_id]; + if (!surf) return {error: "Invalid surface id"}; + tex = new video.texture(renderer, surf); + } + // Create from properties + else if (msg.data.width && msg.data.height) { + tex = new video.texture(renderer, { + width: msg.data.width, + height: msg.data.height, + format: msg.data.format || 'rgba8888', + pixels: msg.data.pixels, + pitch: msg.data.pitch + }); + } + else { + log.console(json.encode(msg.data)) + return {error: "Must provide either surface_id or width/height"}; + } + + // 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, data: {size: tex.size}}; + } + + // All other operations require a valid texture ID + if (!msg.id || !resources.texture[msg.id]) { + return {error: "Invalid texture id: " + msg.id}; + } + + var tex = resources.texture[msg.id]; + + switch (msg.op) { + case 'destroy': + delete resources.texture[msg.id]; + // Texture is automatically destroyed when all references are gone + return {success: true}; + + case 'get': + var prop = msg.data ? msg.data.property : null; + if (!prop) return {error: "Missing property name"}; + return {data: tex[prop]}; + + case 'set': + var prop = msg.data ? msg.data.property : null; + var value = msg.data ? msg.data.value : null; + if (!prop) return {error: "Missing property name"}; + + // Validate property is settable + var readonly = ['size', 'width', 'height']; + if (readonly.indexOf(prop) != -1) { + return {error: "Property '" + prop + "' is read-only"}; + } + + tex[prop] = value; + return {success: true}; + + case 'update': + if (!msg.data) return {error: "Missing update data"}; + tex.update( + msg.data.rect || null, + msg.data.pixels, + msg.data.pitch || 0 + ); + return {success: true}; + + case 'lock': + var result = tex.lock(msg.data ? msg.data.rect : null); + return {data: result}; + + case 'unlock': + tex.unlock(); + return {success: true}; + + case 'query': + return {data: tex.query()}; + + default: + return {error: "Unknown texture operation: " + msg.op}; + } +} + +// Surface operations (mainly for cleanup) +function handle_surface(msg) { + switch (msg.op) { + case 'destroy': + if (!msg.id || !resources.surface[msg.id]) { + return {error: "Invalid surface id: " + msg.id}; + } + delete resources.surface[msg.id]; + return {success: true}; + + default: + return {error: "Unknown surface operation: " + msg.op}; + } +} + +// Cursor operations +function handle_cursor(msg) { + switch (msg.op) { + case 'create': + var surf = new surface(msg.data) + + var hotspot = msg.data.hotspot || [0, 0]; + var cursor = video.createCursor(surf, hotspot); + + var cursor_id = allocate_id(); + resources.cursor[cursor_id] = cursor; + return {id: cursor_id}; + + case 'set': + var cursor = null; + if (msg.id && resources.cursor[msg.id]) { + cursor = resources.cursor[msg.id]; + } + video.setCursor(cursor); + return {success: true}; + + case 'destroy': + if (!msg.id || !resources.cursor[msg.id]) { + return {error: "Invalid cursor id: " + msg.id}; + } + delete resources.cursor[msg.id]; + return {success: true}; + + default: + return {error: "Unknown cursor operation: " + msg.op}; + } +} + +// Utility function to create window and renderer +prosperon.endowments = prosperon.endowments || {}; + +// Mouse operations +function handle_mouse(msg) { + var mouse = video.mouse; + + switch (msg.op) { + case 'show': + if (msg.data == null) return {error: "Missing show parameter"}; + mouse.show(msg.data); + return {success: true}; + + case 'capture': + if (msg.data == null) return {error: "Missing capture parameter"}; + mouse.capture(msg.data); + return {success: true}; + + case 'get_state': + return {data: mouse.get_state()}; + + case 'get_global_state': + return {data: mouse.get_global_state()}; + + case 'get_relative_state': + return {data: mouse.get_relative_state()}; + + case 'warp_global': + if (!msg.data) return {error: "Missing position"}; + mouse.warp_global(msg.data); + return {success: true}; + + case 'warp_in_window': + if (!msg.data || !msg.data.window_id || !msg.data.pos) + return {error: "Missing window_id or position"}; + var window = resources.window[msg.data.window_id]; + if (!window) return {error: "Invalid window id"}; + mouse.warp_in_window(window, msg.data.pos); + return {success: true}; + + case 'cursor_visible': + return {data: mouse.cursor_visible()}; + + case 'get_cursor': + var cursor = mouse.get_cursor(); + if (!cursor) return {data: null}; + // Find or create cursor ID + for (var id in resources.cursor) { + if (resources.cursor[id] == cursor) { + return {data: id}; + } + } + // Not tracked, add it + var cursor_id = allocate_id(); + resources.cursor[cursor_id] = cursor; + return {data: cursor_id}; + + case 'get_default_cursor': + var cursor = mouse.get_default_cursor(); + if (!cursor) return {data: null}; + // Find or create cursor ID + for (var id in resources.cursor) { + if (resources.cursor[id] == cursor) { + return {data: id}; + } + } + // Not tracked, add it + var cursor_id = allocate_id(); + resources.cursor[cursor_id] = cursor; + return {data: cursor_id}; + + case 'create_system_cursor': + if (msg.data == null) return {error: "Missing cursor type"}; + var cursor = mouse.create_system_cursor(msg.data); + var cursor_id = allocate_id(); + resources.cursor[cursor_id] = cursor; + return {id: cursor_id}; + + case 'get_focus': + var window = mouse.get_focus(); + if (!window) return {data: null}; + // Find window ID + for (var id in resources.window) { + if (resources.window[id] == window) { + return {data: id}; + } + } + // Not tracked, add it + var win_id = allocate_id(); + resources.window[win_id] = window; + return {data: win_id}; + + default: + return {error: "Unknown mouse operation: " + msg.op}; + } +} + +// Keyboard operations +function handle_keyboard(msg) { + var keyboard = video.keyboard; + + switch (msg.op) { + case 'get_state': + return {data: keyboard.get_state()}; + + case 'get_focus': + var window = keyboard.get_focus(); + if (!window) return {data: null}; + // Find window ID + for (var id in resources.window) { + if (resources.window[id] == window) { + return {data: id}; + } + } + // Not tracked, add it + var win_id = allocate_id(); + resources.window[win_id] = window; + return {data: win_id}; + + case 'start_text_input': + var window = null; + if (msg.data && msg.data.window_id) { + window = resources.window[msg.data.window_id]; + if (!window) return {error: "Invalid window id"}; + } + keyboard.start_text_input(window); + return {success: true}; + + case 'stop_text_input': + var window = null; + if (msg.data && msg.data.window_id) { + window = resources.window[msg.data.window_id]; + if (!window) return {error: "Invalid window id"}; + } + keyboard.stop_text_input(window); + return {success: true}; + + case 'text_input_active': + var window = null; + if (msg.data && msg.data.window_id) { + window = resources.window[msg.data.window_id]; + if (!window) return {error: "Invalid window id"}; + } + return {data: keyboard.text_input_active(window)}; + + case 'get_text_input_area': + var window = null; + if (msg.data && msg.data.window_id) { + window = resources.window[msg.data.window_id]; + if (!window) return {error: "Invalid window id"}; + } + return {data: keyboard.get_text_input_area(window)}; + + case 'set_text_input_area': + if (!msg.data || !msg.data.rect) return {error: "Missing rect"}; + var window = null; + if (msg.data.window_id) { + window = resources.window[msg.data.window_id]; + if (!window) return {error: "Invalid window id"}; + } + keyboard.set_text_input_area(msg.data.rect, msg.data.cursor || 0, window); + return {success: true}; + + case 'clear_composition': + var window = null; + if (msg.data && msg.data.window_id) { + window = resources.window[msg.data.window_id]; + if (!window) return {error: "Invalid window id"}; + } + keyboard.clear_composition(window); + return {success: true}; + + case 'screen_keyboard_shown': + if (!msg.data || !msg.data.window_id) return {error: "Missing window_id"}; + var window = resources.window[msg.data.window_id]; + if (!window) return {error: "Invalid window id"}; + return {data: keyboard.screen_keyboard_shown(window)}; + + case 'reset': + keyboard.reset(); + return {success: true}; + + default: + return {error: "Unknown keyboard operation: " + msg.op}; + } +} diff --git a/shaders/circle.frag.hlsl b/shaders/circle.frag.hlsl new file mode 100644 index 00000000..c0452f75 --- /dev/null +++ b/shaders/circle.frag.hlsl @@ -0,0 +1,13 @@ +#include "common/pixel.hlsl" +#include "common/sdf.hlsl" + +// Pixel shader main function +float4 main(PSInput input) : SV_TARGET +{ + float4 color = input.color; + float2 p = input.uv; + p -= 0.5; + p *= 2; + color.a = abs(sdf.circle(p, 1)) <= 0.005 ? 1:0 ; + return color; +} \ No newline at end of file diff --git a/shaders/common/common.hlsl b/shaders/common/common.hlsl new file mode 100644 index 00000000..83bde469 --- /dev/null +++ b/shaders/common/common.hlsl @@ -0,0 +1,16 @@ +// Constant Buffer for Transformation Matrices +cbuffer TransformBuffer : register(b0, space1) +{ + float4x4 world_to_projection; + float4x4 projection_to_world; + float4x4 world_to_view; + float4x4 view_to_projection; + float3 camera_pos_world; + float viewport_min_z; + float3 camera_dir_world; + float viewport_max_z; + float2 viewport_size; + float2 viewport_offset; + float2 render_size; + float time; // time in seconds since app start +} diff --git a/shaders/common/model_pixel.hlsl b/shaders/common/model_pixel.hlsl new file mode 100644 index 00000000..0fbee7e0 --- /dev/null +++ b/shaders/common/model_pixel.hlsl @@ -0,0 +1,10 @@ + +struct PSInput +{ + float4 pos : SV_POSITION; + float2 uv : TEXCOORD0; + float4 color : COLOR0; + float3 normal : NORMAL; + float3 gouraud : COLOR1; +}; + diff --git a/shaders/common/model_vertex.hlsl b/shaders/common/model_vertex.hlsl new file mode 100644 index 00000000..3767d19d --- /dev/null +++ b/shaders/common/model_vertex.hlsl @@ -0,0 +1,42 @@ +#include "common.hlsl" + +struct input +{ + float3 pos : pos; + float2 uv : uv; + float4 color : color; + float3 normal : norm; +}; + +struct output +{ + float4 pos : SV_POSITION; + float2 uv: TEXCOORD0; + float4 color : COLOR0; + float3 normal : NORMAL; + float3 gouraud : COLOR1; +}; + +float3 ps1_directional_light(float3 normal, float3 direction, float3 ambient, float3 lightcolor) +{ + float NdotL = dot(normal, normalize(-direction)); + NdotL = max(NdotL, 0.0f); + float3 color = ambient + lightcolor*NdotL; + return color; +} + +output main(input i) +{ + output o; + o.pos = mul(world_to_projection, float4(i.pos,1.0)); + o.uv = i.uv; + o.color = i.color; + o.normal = i.normal; + + float3 ambient = float3(0.2,0.2,0.2); + float3 lightdir = normalize(float3(1,1,1)); + o.gouraud = ps1_directional_light(i.normal, lightdir, ambient, float3(1,1,1)); + + + return o; +} diff --git a/shaders/common/pixel.hlsl b/shaders/common/pixel.hlsl new file mode 100644 index 00000000..2c6c3aee --- /dev/null +++ b/shaders/common/pixel.hlsl @@ -0,0 +1,8 @@ +#include "common.hlsl" + +// Structure for pixel shader input (from vertex shader output). +struct PSInput +{ + float2 uv : TEXCOORD0; + float4 color : COLOR0; +}; diff --git a/shaders/common/sdf.hlsl b/shaders/common/sdf.hlsl new file mode 100644 index 00000000..0a2d20b8 --- /dev/null +++ b/shaders/common/sdf.hlsl @@ -0,0 +1,41 @@ +struct SDF { + +float dot2(float2 a) +{ + return dot(a,a); +} + +float circle(float2 p, float r) +{ + return length(p) - r; +} + +// p = uv point +// b = width,height +// r = roundedness of the 4 corners +float rounded_box(float2 p, float2 b, float4 r) +{ + r.xy = (p.x>0.0)?r.xy : r.zw; + r.x = (p.y>0.0)?r.x : r.y; + float2 q = abs(p)-b+r.x; + return min(max(q.x,q.y),0.0) + length(max(q,0.0)) - r.x; +} + +float box(float2 p, float2 b) +{ + float2 d = abs(p)-b; + return length(max(d,0)) + min(max(d.x,d.y),0); +} + +float heart( in float2 p ) +{ + p.x = abs(p.x); + + if( p.y+p.x>1.0 ) + return sqrt(dot2(p-float2(0.25,0.75))) - sqrt(2.0)/4.0; + + return sqrt(min(dot2(p-float2(0.00,1.00)), dot2(p-0.5*max(p.x+p.y,0.0)))) * sign(p.x-p.y); +} +}; + +SDF sdf; \ No newline at end of file diff --git a/shaders/common/vertex.hlsl b/shaders/common/vertex.hlsl new file mode 100644 index 00000000..c063e273 --- /dev/null +++ b/shaders/common/vertex.hlsl @@ -0,0 +1,32 @@ +#include "common.hlsl" + +struct input +{ + float2 pos : pos; // given as world + float2 uv : uv; // always a quad + float4 color : color; +}; + +// Output structure from the vertex shader to the pixel shader +struct output +{ + float4 pos : SV_Position; // Clip-space position + float2 uv : TEXCOORD0; // Texture coordinates + float4 color : COLOR0; // Interpolated vertex color +}; + +cbuffer model : register(b1, space1) +{ + float4x4 model; + float4 color; +}; + +output main(input i) +{ + output o; + float4 worldpos = mul(model, float4(i.pos,0,1)); + o.pos = mul(world_to_projection, worldpos); + o.uv = i.uv; + o.color = i.color * color; + return o; +} \ No newline at end of file diff --git a/shaders/compile.sh b/shaders/compile.sh new file mode 100755 index 00000000..eed2c669 --- /dev/null +++ b/shaders/compile.sh @@ -0,0 +1,55 @@ +#!/usr/bin/env bash + +# Ensure directories exist +mkdir -p spv +mkdir -p msl +mkdir -p dxil +mkdir -p reflection + +# Vertex shaders +for filename in *.vert.hlsl; do + if [ -f "$filename" ]; then + outSpv="spv/${filename/.hlsl/.spv}" + outDxil="dxil/${filename/.hlsl/.dxil}" + outMsl="msl/${filename/.hlsl/.msl}" + outReflect="reflection/${filename/.hlsl/.json}" + # Produce SPIR-V + dxc -spirv -T vs_6_0 -Fo "$outSpv" "$filename" + # Produce DXIL with embedded debug info (example) + shadercross "$filename" -o "$outDxil" +# dxc -Zi -Qembed_debug -T vs_6_0 -Fo "$outDxil" "$filename" + # Convert SPIR-V to Metal Shader Language + spirv-cross "$outSpv" --msl > "$outMsl" + # Generate reflection + spirv-cross "$outSpv" --reflect > "$outReflect" + fi +done + +# Fragment shaders +for filename in *.frag.hlsl; do + if [ -f "$filename" ]; then + outSpv="spv/${filename/.hlsl/.spv}" + outDxil="dxil/${filename/.hlsl/.dxil}" + outMsl="msl/${filename/.hlsl/.msl}" + outReflect="reflection/${filename/.hlsl/.json}" + dxc -spirv -T ps_6_0 -Fo "$outSpv" "$filename" + shadercross "$filename" -o "$outDxil" +# dxc -Zi -Qembed_debug -T ps_6_0 -Fo "$outDxil" "$filename" + spirv-cross "$outSpv" --msl > "$outMsl" + spirv-cross "$outSpv" --reflect > "$outReflect" + fi +done + +# Compute shaders +for filename in *.comp.hlsl; do + if [ -f "$filename" ]; then + outSpv="spv/${filename/.hlsl/.spv}" + outDxil="dxil/${filename/.hlsl/.dxil}" + outMsl="msl/${filename/.hlsl/.msl}" + outReflect="reflection/${filename/.hlsl/.json}" + dxc -spirv -T cs_6_0 -Fo "$outSpv" "$filename" + dxc -Zi -Qembed_debug -T cs_6_0 -Fo "$outDxil" "$filename" + spirv-cross "$outSpv" --msl > "$outMsl" + spirv-cross "$outSpv" --reflect > "$outReflect" + fi +done diff --git a/shaders/dbgline.frag.hlsl b/shaders/dbgline.frag.hlsl new file mode 100644 index 00000000..b8083767 --- /dev/null +++ b/shaders/dbgline.frag.hlsl @@ -0,0 +1,12 @@ +#include "common/common.hlsl" + +struct VSOutput +{ + float4 pos : SV_POSITION; + float4 color : COLOR; +}; + +float4 main(VSOutput input) : SV_TARGET +{ + return input.color; +} \ No newline at end of file diff --git a/shaders/dbgline.vert.hlsl b/shaders/dbgline.vert.hlsl new file mode 100644 index 00000000..b3e7d0a7 --- /dev/null +++ b/shaders/dbgline.vert.hlsl @@ -0,0 +1,22 @@ +#include "common/common.hlsl" + +struct VSInput +{ + float3 pos : pos; + float4 color : color; +}; + +struct VSOutput +{ + float4 pos : SV_POSITION; + float4 color : COLOR; +}; + +VSOutput main(VSInput input) +{ + VSOutput output; + output.pos = mul(float4(input.pos, 1.0f), world_to_projection); + output.color = input.color; + output.color.r = frac(time); + return output; +} \ No newline at end of file diff --git a/shaders/dxil/dbgline.frag.dxil b/shaders/dxil/dbgline.frag.dxil new file mode 100644 index 00000000..105930fa Binary files /dev/null and b/shaders/dxil/dbgline.frag.dxil differ diff --git a/shaders/dxil/dbgline.vert.dxil b/shaders/dxil/dbgline.vert.dxil new file mode 100644 index 00000000..d835806d Binary files /dev/null and b/shaders/dxil/dbgline.vert.dxil differ diff --git a/shaders/dxil/model.frag.dxil b/shaders/dxil/model.frag.dxil new file mode 100644 index 00000000..c13a7636 Binary files /dev/null and b/shaders/dxil/model.frag.dxil differ diff --git a/shaders/dxil/model.vert.dxil b/shaders/dxil/model.vert.dxil new file mode 100644 index 00000000..f1e54c03 Binary files /dev/null and b/shaders/dxil/model.vert.dxil differ diff --git a/shaders/dxil/model_lit.frag.dxil b/shaders/dxil/model_lit.frag.dxil new file mode 100644 index 00000000..e6704aec Binary files /dev/null and b/shaders/dxil/model_lit.frag.dxil differ diff --git a/shaders/dxil/post.frag.dxil b/shaders/dxil/post.frag.dxil new file mode 100644 index 00000000..f575e01b Binary files /dev/null and b/shaders/dxil/post.frag.dxil differ diff --git a/shaders/dxil/post.vert.dxil b/shaders/dxil/post.vert.dxil new file mode 100644 index 00000000..e66a6b82 Binary files /dev/null and b/shaders/dxil/post.vert.dxil differ diff --git a/shaders/dxil/ps1.frag.dxil b/shaders/dxil/ps1.frag.dxil new file mode 100644 index 00000000..e69de29b diff --git a/shaders/dxil/ps1.vert.dxil b/shaders/dxil/ps1.vert.dxil new file mode 100644 index 00000000..4abd9aa9 Binary files /dev/null and b/shaders/dxil/ps1.vert.dxil differ diff --git a/shaders/dxil/rectangle.frag.dxil b/shaders/dxil/rectangle.frag.dxil new file mode 100644 index 00000000..e9cc33fc Binary files /dev/null and b/shaders/dxil/rectangle.frag.dxil differ diff --git a/shaders/dxil/sprite.frag.dxil b/shaders/dxil/sprite.frag.dxil new file mode 100644 index 00000000..171839cd Binary files /dev/null and b/shaders/dxil/sprite.frag.dxil differ diff --git a/shaders/dxil/sprite.vert.dxil b/shaders/dxil/sprite.vert.dxil new file mode 100644 index 00000000..4abd9aa9 Binary files /dev/null and b/shaders/dxil/sprite.vert.dxil differ diff --git a/shaders/model.frag.hlsl b/shaders/model.frag.hlsl new file mode 100644 index 00000000..faa13734 --- /dev/null +++ b/shaders/model.frag.hlsl @@ -0,0 +1,12 @@ +// If using a texture instead, define: +#include "common/model_pixel.hlsl" +Texture2D diffuse : register(t0, space2); +SamplerState smp : register(s0, space2); + + +float4 main(PSInput input) : SV_TARGET +{ + float4 color = diffuse.Sample(smp, input.uv); + + return float4(color.rgb*input.gouraud, 1.0); +} diff --git a/shaders/model.vert.hlsl b/shaders/model.vert.hlsl new file mode 100644 index 00000000..328a9274 --- /dev/null +++ b/shaders/model.vert.hlsl @@ -0,0 +1,6 @@ +#include "common/model_vertex.hlsl" + +output vertex(output o) +{ + return o; +} diff --git a/shaders/model_lit.frag.hlsl b/shaders/model_lit.frag.hlsl new file mode 100644 index 00000000..b5ce2205 --- /dev/null +++ b/shaders/model_lit.frag.hlsl @@ -0,0 +1,40 @@ +cbuffer LightBuffer : register(b2, space1) +{ + float4 uDiffuseColor; // base diffuse color + float3 uLightDirection; // direction of the incoming light, normalized + float4 uLightColor; // color of the light, e.g., (1,1,1,1) +}; + +// Example hard-coded values (if not using cbuffer, you can directly hardcode in code): +// float3 uLightDirection = normalize(float3(0.0, -1.0, -1.0)); +// float4 uLightColor = float4(1.0, 1.0, 1.0, 1.0); + +// You can also still use a texture: +Texture2D uDiffuseTexture : register(t0); +SamplerState samplerState : register(s0); + +struct PSInput +{ + float4 pos : SV_POSITION; + float2 uv : TEXCOORD0; + float3 normal : NORMAL; +}; + +float4 main(PSInput input) : SV_TARGET +{ + // Sample base color + float4 baseColor = uDiffuseTexture.Sample(samplerState, input.uv); + // If no texture, just use uDiffuseColor: + // float4 baseColor = uDiffuseColor; + + // Compute diffuse lighting + // Ensure normal is normalized + float3 N = normalize(input.normal); + // Light direction is assumed normalized. Make sure uLightDirection is a direction from fragment to light. + // If the direction is from the light to the fragment, invert it: float3 L = -uLightDirection; + float NdotL = saturate(dot(N, -uLightDirection)); + + float4 shadedColor = baseColor * (uLightColor * NdotL); + + return shadedColor; +} diff --git a/shaders/msl/dbgline.frag.msl b/shaders/msl/dbgline.frag.msl new file mode 100644 index 00000000..5095153b --- /dev/null +++ b/shaders/msl/dbgline.frag.msl @@ -0,0 +1,22 @@ +#include +#include + +using namespace metal; + +struct main0_out +{ + float4 out_var_SV_TARGET [[color(0)]]; +}; + +struct main0_in +{ + float4 in_var_COLOR [[user(locn0)]]; +}; + +fragment main0_out main0(main0_in in [[stage_in]]) +{ + main0_out out = {}; + out.out_var_SV_TARGET = in.in_var_COLOR; + return out; +} + diff --git a/shaders/msl/dbgline.vert.msl b/shaders/msl/dbgline.vert.msl new file mode 100644 index 00000000..978adabb --- /dev/null +++ b/shaders/msl/dbgline.vert.msl @@ -0,0 +1,43 @@ +#include +#include + +using namespace metal; + +struct type_TransformBuffer +{ + float4x4 world_to_projection; + float4x4 projection_to_world; + float4x4 world_to_view; + float4x4 view_to_projection; + packed_float3 camera_pos_world; + float viewport_min_z; + packed_float3 camera_dir_world; + float viewport_max_z; + float2 viewport_size; + float2 viewport_offset; + float2 render_size; + float time; +}; + +struct main0_out +{ + float4 out_var_COLOR [[user(locn0)]]; + float4 gl_Position [[position]]; +}; + +struct main0_in +{ + float3 in_var_pos [[attribute(0)]]; + float4 in_var_color [[attribute(1)]]; +}; + +vertex main0_out main0(main0_in in [[stage_in]], constant type_TransformBuffer& TransformBuffer [[buffer(0)]]) +{ + main0_out out = {}; + float4 _28 = in.in_var_color; + _28.x = fract(TransformBuffer.time); + out.gl_Position = float4(in.in_var_pos, 1.0) * TransformBuffer.world_to_projection; + out.out_var_COLOR = _28; + return out; +} + diff --git a/shaders/msl/model.frag.msl b/shaders/msl/model.frag.msl new file mode 100644 index 00000000..03d67f1d --- /dev/null +++ b/shaders/msl/model.frag.msl @@ -0,0 +1,23 @@ +#include +#include + +using namespace metal; + +struct main0_out +{ + float4 out_var_SV_TARGET [[color(0)]]; +}; + +struct main0_in +{ + float2 in_var_TEXCOORD0 [[user(locn0)]]; + float3 in_var_COLOR1 [[user(locn3)]]; +}; + +fragment main0_out main0(main0_in in [[stage_in]], texture2d diffuse [[texture(0)]], sampler smp [[sampler(0)]]) +{ + main0_out out = {}; + out.out_var_SV_TARGET = float4(diffuse.sample(smp, in.in_var_TEXCOORD0).xyz * in.in_var_COLOR1, 1.0); + return out; +} + diff --git a/shaders/msl/model.vert.msl b/shaders/msl/model.vert.msl new file mode 100644 index 00000000..c1d3cd14 --- /dev/null +++ b/shaders/msl/model.vert.msl @@ -0,0 +1,49 @@ +#include +#include + +using namespace metal; + +struct type_TransformBuffer +{ + float4x4 world_to_projection; + float4x4 projection_to_world; + float4x4 world_to_view; + float4x4 view_to_projection; + packed_float3 camera_pos_world; + float viewport_min_z; + packed_float3 camera_dir_world; + float viewport_max_z; + float2 viewport_size; + float2 viewport_offset; + float2 render_size; + float time; +}; + +struct main0_out +{ + float2 out_var_TEXCOORD0 [[user(locn0)]]; + float4 out_var_COLOR0 [[user(locn1)]]; + float3 out_var_NORMAL [[user(locn2)]]; + float3 out_var_COLOR1 [[user(locn3)]]; + float4 gl_Position [[position]]; +}; + +struct main0_in +{ + float3 in_var_pos [[attribute(0)]]; + float2 in_var_uv [[attribute(1)]]; + float4 in_var_color [[attribute(2)]]; + float3 in_var_norm [[attribute(3)]]; +}; + +vertex main0_out main0(main0_in in [[stage_in]], constant type_TransformBuffer& TransformBuffer [[buffer(0)]]) +{ + main0_out out = {}; + out.gl_Position = TransformBuffer.world_to_projection * float4(in.in_var_pos, 1.0); + out.out_var_TEXCOORD0 = in.in_var_uv; + out.out_var_COLOR0 = in.in_var_color; + out.out_var_NORMAL = in.in_var_norm; + out.out_var_COLOR1 = float3(0.20000000298023223876953125) + (float3(1.0) * precise::max(dot(in.in_var_norm, fast::normalize(-fast::normalize(float3(1.0)))), 0.0)); + return out; +} + diff --git a/shaders/msl/model_lit.frag.msl b/shaders/msl/model_lit.frag.msl new file mode 100644 index 00000000..6b03c511 --- /dev/null +++ b/shaders/msl/model_lit.frag.msl @@ -0,0 +1,30 @@ +#include +#include + +using namespace metal; + +struct type_LightBuffer +{ + float4 uDiffuseColor; + float3 uLightDirection; + float4 uLightColor; +}; + +struct main0_out +{ + float4 out_var_SV_TARGET [[color(0)]]; +}; + +struct main0_in +{ + float2 in_var_TEXCOORD0 [[user(locn0)]]; + float3 in_var_NORMAL [[user(locn1)]]; +}; + +fragment main0_out main0(main0_in in [[stage_in]], constant type_LightBuffer& LightBuffer [[buffer(0)]], texture2d uDiffuseTexture [[texture(0)]], sampler samplerState [[sampler(0)]]) +{ + main0_out out = {}; + out.out_var_SV_TARGET = uDiffuseTexture.sample(samplerState, in.in_var_TEXCOORD0) * (LightBuffer.uLightColor * fast::clamp(dot(fast::normalize(in.in_var_NORMAL), -LightBuffer.uLightDirection), 0.0, 1.0)); + return out; +} + diff --git a/shaders/msl/post.frag.msl b/shaders/msl/post.frag.msl new file mode 100644 index 00000000..ae298675 --- /dev/null +++ b/shaders/msl/post.frag.msl @@ -0,0 +1,22 @@ +#include +#include + +using namespace metal; + +struct main0_out +{ + float4 out_var_SV_TARGET [[color(0)]]; +}; + +struct main0_in +{ + float2 in_var_TEXCOORD0 [[user(locn0)]]; +}; + +fragment main0_out main0(main0_in in [[stage_in]], texture2d diffuse [[texture(0)]], sampler smp [[sampler(0)]]) +{ + main0_out out = {}; + out.out_var_SV_TARGET = diffuse.sample(smp, in.in_var_TEXCOORD0); + return out; +} + diff --git a/shaders/msl/post.vert.msl b/shaders/msl/post.vert.msl new file mode 100644 index 00000000..6fea5760 --- /dev/null +++ b/shaders/msl/post.vert.msl @@ -0,0 +1,41 @@ +#include +#include + +using namespace metal; + +struct type_TransformBuffer +{ + float4x4 world_to_projection; + float4x4 projection_to_world; + float4x4 world_to_view; + float4x4 view_to_projection; + packed_float3 camera_pos_world; + float viewport_min_z; + packed_float3 camera_dir_world; + float viewport_max_z; + float2 viewport_size; + float2 viewport_offset; + float2 render_size; + float time; +}; + +struct main0_out +{ + float2 out_var_TEXCOORD0 [[user(locn0)]]; + float4 gl_Position [[position]]; +}; + +struct main0_in +{ + float2 in_var_pos [[attribute(0)]]; + float2 in_var_uv [[attribute(1)]]; +}; + +vertex main0_out main0(main0_in in [[stage_in]], constant type_TransformBuffer& TransformBuffer [[buffer(0)]]) +{ + main0_out out = {}; + out.gl_Position = TransformBuffer.world_to_projection * float4(in.in_var_pos, 0.0, 1.0); + out.out_var_TEXCOORD0 = in.in_var_uv; + return out; +} + diff --git a/shaders/msl/ps1.frag.msl b/shaders/msl/ps1.frag.msl new file mode 100644 index 00000000..968869dc --- /dev/null +++ b/shaders/msl/ps1.frag.msl @@ -0,0 +1,28 @@ +#include +#include + +using namespace metal; + +struct type_Globals +{ + float4 ditherPattern2x2[4]; +}; + +struct main0_out +{ + float4 out_var_SV_TARGET [[color(0)]]; +}; + +struct main0_in +{ + float2 in_var_TEXCOORD0 [[user(locn0)]]; +}; + +fragment main0_out main0(main0_in in [[stage_in]], constant type_Globals& _Globals [[buffer(0)]], texture2d diffuseTexture [[texture(0)]], sampler smp [[sampler(0)]], float4 gl_FragCoord [[position]]) +{ + main0_out out = {}; + float4 _61 = fast::clamp(diffuseTexture.sample(smp, in.in_var_TEXCOORD0) + float4(0.03125 * _Globals.ditherPattern2x2[((int(gl_FragCoord.y) & 1) * 2) + (int(gl_FragCoord.x) & 1)].x), float4(0.0), float4(1.0)); + out.out_var_SV_TARGET = float4(rint(_61.xyz * 31.0) * float3(0.0322580635547637939453125), rint(_61.w * 31.0) * 0.0322580635547637939453125); + return out; +} + diff --git a/shaders/msl/ps1.vert.msl b/shaders/msl/ps1.vert.msl new file mode 100644 index 00000000..221a41ed --- /dev/null +++ b/shaders/msl/ps1.vert.msl @@ -0,0 +1,50 @@ +#include +#include + +using namespace metal; + +struct type_TransformBuffer +{ + float4x4 world_to_projection; + float4x4 projection_to_world; + float4x4 world_to_view; + float4x4 view_to_projection; + packed_float3 camera_pos_world; + float viewport_min_z; + packed_float3 camera_dir_world; + float viewport_max_z; + float2 viewport_size; + float2 viewport_offset; + float2 render_size; + float time; +}; + +struct type_model +{ + float4x4 model; + float4 color; +}; + +struct main0_out +{ + float2 out_var_TEXCOORD0 [[user(locn0)]]; + float4 out_var_COLOR0 [[user(locn1)]]; + float4 gl_Position [[position]]; +}; + +struct main0_in +{ + float2 in_var_pos [[attribute(0)]]; + float2 in_var_uv [[attribute(1)]]; + float4 in_var_color [[attribute(2)]]; +}; + +vertex main0_out main0(main0_in in [[stage_in]], constant type_TransformBuffer& TransformBuffer [[buffer(0)]], constant type_model& model [[buffer(1)]]) +{ + main0_out out = {}; + out.gl_Position = TransformBuffer.world_to_projection * (model.model * float4(in.in_var_pos, 0.0, 1.0)); + out.out_var_TEXCOORD0 = in.in_var_uv; + out.out_var_COLOR0 = in.in_var_color * model.color; + return out; +} + diff --git a/shaders/msl/rectangle.frag.msl b/shaders/msl/rectangle.frag.msl new file mode 100644 index 00000000..bc52ad00 --- /dev/null +++ b/shaders/msl/rectangle.frag.msl @@ -0,0 +1,22 @@ +#include +#include + +using namespace metal; + +struct main0_out +{ + float4 out_var_SV_TARGET [[color(0)]]; +}; + +struct main0_in +{ + float4 in_var_COLOR0 [[user(locn1)]]; +}; + +fragment main0_out main0(main0_in in [[stage_in]]) +{ + main0_out out = {}; + out.out_var_SV_TARGET = in.in_var_COLOR0; + return out; +} + diff --git a/shaders/msl/sprite.frag.msl b/shaders/msl/sprite.frag.msl new file mode 100644 index 00000000..a08fa705 --- /dev/null +++ b/shaders/msl/sprite.frag.msl @@ -0,0 +1,23 @@ +#include +#include + +using namespace metal; + +struct main0_out +{ + float4 out_var_SV_TARGET [[color(0)]]; +}; + +struct main0_in +{ + float2 in_var_TEXCOORD0 [[user(locn0)]]; + float4 in_var_COLOR0 [[user(locn1)]]; +}; + +fragment main0_out main0(main0_in in [[stage_in]], texture2d diffuse [[texture(0)]], sampler smp [[sampler(0)]]) +{ + main0_out out = {}; + out.out_var_SV_TARGET = diffuse.sample(smp, in.in_var_TEXCOORD0) * in.in_var_COLOR0; + return out; +} + diff --git a/shaders/msl/sprite.vert.msl b/shaders/msl/sprite.vert.msl new file mode 100644 index 00000000..221a41ed --- /dev/null +++ b/shaders/msl/sprite.vert.msl @@ -0,0 +1,50 @@ +#include +#include + +using namespace metal; + +struct type_TransformBuffer +{ + float4x4 world_to_projection; + float4x4 projection_to_world; + float4x4 world_to_view; + float4x4 view_to_projection; + packed_float3 camera_pos_world; + float viewport_min_z; + packed_float3 camera_dir_world; + float viewport_max_z; + float2 viewport_size; + float2 viewport_offset; + float2 render_size; + float time; +}; + +struct type_model +{ + float4x4 model; + float4 color; +}; + +struct main0_out +{ + float2 out_var_TEXCOORD0 [[user(locn0)]]; + float4 out_var_COLOR0 [[user(locn1)]]; + float4 gl_Position [[position]]; +}; + +struct main0_in +{ + float2 in_var_pos [[attribute(0)]]; + float2 in_var_uv [[attribute(1)]]; + float4 in_var_color [[attribute(2)]]; +}; + +vertex main0_out main0(main0_in in [[stage_in]], constant type_TransformBuffer& TransformBuffer [[buffer(0)]], constant type_model& model [[buffer(1)]]) +{ + main0_out out = {}; + out.gl_Position = TransformBuffer.world_to_projection * (model.model * float4(in.in_var_pos, 0.0, 1.0)); + out.out_var_TEXCOORD0 = in.in_var_uv; + out.out_var_COLOR0 = in.in_var_color * model.color; + return out; +} + diff --git a/shaders/post.frag.hlsl b/shaders/post.frag.hlsl new file mode 100644 index 00000000..de5bc1f9 --- /dev/null +++ b/shaders/post.frag.hlsl @@ -0,0 +1,11 @@ +#include "common/pixel.hlsl" + +Texture2D diffuse : register(t0, space2); +SamplerState smp : register(s0, space2); + +// Pixel shader main function +float4 main(PSInput input) : SV_TARGET +{ + float4 color = diffuse.Sample(smp, input.uv); + return color; +} \ No newline at end of file diff --git a/shaders/post.vert.hlsl b/shaders/post.vert.hlsl new file mode 100644 index 00000000..dea2451f --- /dev/null +++ b/shaders/post.vert.hlsl @@ -0,0 +1,21 @@ +#include "common/common.hlsl" + +struct input +{ + float2 pos : pos; + float2 uv : uv; +}; + +struct output +{ + float4 pos : SV_Position; + float2 uv : TEXCOORD0; +}; + +output main(input i) +{ + output o; + o.pos = mul(world_to_projection, float4(i.pos, 0, 1)); + o.uv = i.uv; + return o; +} \ No newline at end of file diff --git a/shaders/ps1.frag.hlsl b/shaders/ps1.frag.hlsl new file mode 100644 index 00000000..f3105e1e --- /dev/null +++ b/shaders/ps1.frag.hlsl @@ -0,0 +1,56 @@ +Texture2D diffuseTexture : register(t0,space2); +SamplerState smp : register(s0,space2); + +struct PSInput +{ + float4 position : SV_POSITION; + float2 texcoord : TEXCOORD0; +}; + +float ditherPattern2x2[4] = { + 0.0, 0.5, + 0.75, 0.25 +}; + +float4 main(PSInput input) : SV_TARGET +{ + // Sample texture with nearest-neighbor + float4 color = diffuseTexture.Sample(smp, input.texcoord); + + // Optional: Affine distortion effect + // If you want to simulate affine warping, you could do some screen-space + // dependent manipulation of texcoords. For simplicity, we won't do that here, + // but one trick is to modify the texcoord by input.position.z or w in some + // simplified manner. The vertex shader step is often enough to simulate "no perspective". + + // Dithering (optional): + // Compute a screen-space coordinate from SV_POSITION + // Use a small dithering pattern + int x = (int)input.position.x; + int y = (int)input.position.y; + int idx = (y & 1) * 2 + (x & 1); + float dither = ditherPattern2x2[idx]; + + // To simulate PS1 color quantization (e.g. to 5 bits for R,G,B): + // We'll quantize each channel. + // Suppose colorBitDepth.x = 5 means 5 bits for R/G/B, that’s 32 steps. + float3 colorBitDepth = float3(5,5,5); + float stepsRGB = pow(2.0, colorBitDepth.x); + float stepsA = pow(2.0, colorBitDepth.y); + + // Add dithering before quantization to reduce banding + // Adjust dithering scale if desired + float ditherStrength = 1.0 / stepsRGB; // dither scale can be tweaked + float4 colorDithered = color + ditherStrength * dither; + + // Clamp after dithering + colorDithered = saturate(colorDithered); + + // Quantize + float3 quantizedRGB = round(colorDithered.rgb * (stepsRGB - 1.0)) / (stepsRGB - 1.0); + float quantizedA = round(colorDithered.a * (stepsA - 1.0)) / (stepsA - 1.0); + + float4 finalColor = float4(quantizedRGB, quantizedA); + + return finalColor; +} diff --git a/shaders/ps1.vert.hlsl b/shaders/ps1.vert.hlsl new file mode 100644 index 00000000..42f54ec4 --- /dev/null +++ b/shaders/ps1.vert.hlsl @@ -0,0 +1,49 @@ +#include "common/vertex.hlsl" + +struct VSInput +{ + float3 position : POSITION; + float2 texcoord : TEXCOORD0; +}; + +struct VSOutput +{ + float4 position : SV_POSITION; + float2 texcoord : TEXCOORD0; +}; + +output vertex(output i) +{ + return i; +} +/* +VSOutput mainVS(VSInput input) +{ + VSOutput output; + + // Standard transform + float4 worldPos = mul(float4(input.position, 1.0), gWorldViewProj); + + // Simulate wobble by snapping coordinates to a lower precision grid. + // For a PS1-style effect, we can quantize the projected coordinates. + // For instance, if wobbleIntensity is something small like 1.0 or 0.5, + // multiply, floor, and divide back: + float factor = 1.0 / wobbleIntensity; + worldPos.x = floor(worldPos.x * factor) / factor; + worldPos.y = floor(worldPos.y * factor) / factor; + worldPos.z = floor(worldPos.z * factor) / factor; + worldPos.w = floor(worldPos.w * factor) / factor; + + // Output position + output.position = worldPos; + + // Pass through texture coordinate as-is. + // We do not do perspective correction here intentionally (PS1 didn't). + // PS1 essentially did affine mapping in screen space. To simulate this, + // we can just pass the original texcoords and let the pixel shader + // treat them linearly. + output.texcoord = input.texcoord; + + return output; +} +*/ \ No newline at end of file diff --git a/shaders/rectangle.frag.hlsl b/shaders/rectangle.frag.hlsl new file mode 100644 index 00000000..c01bd042 --- /dev/null +++ b/shaders/rectangle.frag.hlsl @@ -0,0 +1,7 @@ +#include "common/pixel.hlsl" + +// Pixel shader main function +float4 main(PSInput input) : SV_TARGET +{ + return input.color; +} \ No newline at end of file diff --git a/shaders/reflection/dbgline.frag.json b/shaders/reflection/dbgline.frag.json new file mode 100644 index 00000000..7cbc1a14 --- /dev/null +++ b/shaders/reflection/dbgline.frag.json @@ -0,0 +1,22 @@ +{ + "entryPoints" : [ + { + "name" : "main", + "mode" : "frag" + } + ], + "inputs" : [ + { + "type" : "vec4", + "name" : "in.var.COLOR", + "location" : 0 + } + ], + "outputs" : [ + { + "type" : "vec4", + "name" : "out.var.SV_TARGET", + "location" : 0 + } + ] +} \ No newline at end of file diff --git a/shaders/reflection/dbgline.vert.json b/shaders/reflection/dbgline.vert.json new file mode 100644 index 00000000..9c1148b2 --- /dev/null +++ b/shaders/reflection/dbgline.vert.json @@ -0,0 +1,111 @@ +{ + "entryPoints" : [ + { + "name" : "main", + "mode" : "vert" + } + ], + "types" : { + "_7" : { + "name" : "type.TransformBuffer", + "members" : [ + { + "name" : "world_to_projection", + "type" : "mat4", + "offset" : 0, + "matrix_stride" : 16, + "row_major" : true + }, + { + "name" : "projection_to_world", + "type" : "mat4", + "offset" : 64, + "matrix_stride" : 16, + "row_major" : true + }, + { + "name" : "world_to_view", + "type" : "mat4", + "offset" : 128, + "matrix_stride" : 16, + "row_major" : true + }, + { + "name" : "view_to_projection", + "type" : "mat4", + "offset" : 192, + "matrix_stride" : 16, + "row_major" : true + }, + { + "name" : "camera_pos_world", + "type" : "vec3", + "offset" : 256 + }, + { + "name" : "viewport_min_z", + "type" : "float", + "offset" : 268 + }, + { + "name" : "camera_dir_world", + "type" : "vec3", + "offset" : 272 + }, + { + "name" : "viewport_max_z", + "type" : "float", + "offset" : 284 + }, + { + "name" : "viewport_size", + "type" : "vec2", + "offset" : 288 + }, + { + "name" : "viewport_offset", + "type" : "vec2", + "offset" : 296 + }, + { + "name" : "render_size", + "type" : "vec2", + "offset" : 304 + }, + { + "name" : "time", + "type" : "float", + "offset" : 312 + } + ] + } + }, + "inputs" : [ + { + "type" : "vec3", + "name" : "in.var.pos", + "location" : 0 + }, + { + "type" : "vec4", + "name" : "in.var.color", + "location" : 1 + } + ], + "outputs" : [ + { + "type" : "vec4", + "name" : "out.var.COLOR", + "location" : 0 + } + ], + "ubos" : [ + { + "type" : "_7", + "name" : "type.TransformBuffer", + "block_size" : 316, + "set" : 1, + "binding" : 0 + } + ] +} \ No newline at end of file diff --git a/shaders/reflection/model.frag.json b/shaders/reflection/model.frag.json new file mode 100644 index 00000000..e3049a16 --- /dev/null +++ b/shaders/reflection/model.frag.json @@ -0,0 +1,43 @@ +{ + "entryPoints" : [ + { + "name" : "main", + "mode" : "frag" + } + ], + "inputs" : [ + { + "type" : "vec2", + "name" : "in.var.TEXCOORD0", + "location" : 0 + }, + { + "type" : "vec3", + "name" : "in.var.COLOR1", + "location" : 3 + } + ], + "outputs" : [ + { + "type" : "vec4", + "name" : "out.var.SV_TARGET", + "location" : 0 + } + ], + "separate_images" : [ + { + "type" : "texture2D", + "name" : "diffuse", + "set" : 2, + "binding" : 0 + } + ], + "separate_samplers" : [ + { + "type" : "sampler", + "name" : "smp", + "set" : 2, + "binding" : 0 + } + ] +} \ No newline at end of file diff --git a/shaders/reflection/model.vert.json b/shaders/reflection/model.vert.json new file mode 100644 index 00000000..d8949366 --- /dev/null +++ b/shaders/reflection/model.vert.json @@ -0,0 +1,136 @@ +{ + "entryPoints" : [ + { + "name" : "main", + "mode" : "vert" + } + ], + "types" : { + "_12" : { + "name" : "type.TransformBuffer", + "members" : [ + { + "name" : "world_to_projection", + "type" : "mat4", + "offset" : 0, + "matrix_stride" : 16, + "row_major" : true + }, + { + "name" : "projection_to_world", + "type" : "mat4", + "offset" : 64, + "matrix_stride" : 16, + "row_major" : true + }, + { + "name" : "world_to_view", + "type" : "mat4", + "offset" : 128, + "matrix_stride" : 16, + "row_major" : true + }, + { + "name" : "view_to_projection", + "type" : "mat4", + "offset" : 192, + "matrix_stride" : 16, + "row_major" : true + }, + { + "name" : "camera_pos_world", + "type" : "vec3", + "offset" : 256 + }, + { + "name" : "viewport_min_z", + "type" : "float", + "offset" : 268 + }, + { + "name" : "camera_dir_world", + "type" : "vec3", + "offset" : 272 + }, + { + "name" : "viewport_max_z", + "type" : "float", + "offset" : 284 + }, + { + "name" : "viewport_size", + "type" : "vec2", + "offset" : 288 + }, + { + "name" : "viewport_offset", + "type" : "vec2", + "offset" : 296 + }, + { + "name" : "render_size", + "type" : "vec2", + "offset" : 304 + }, + { + "name" : "time", + "type" : "float", + "offset" : 312 + } + ] + } + }, + "inputs" : [ + { + "type" : "vec3", + "name" : "in.var.pos", + "location" : 0 + }, + { + "type" : "vec2", + "name" : "in.var.uv", + "location" : 1 + }, + { + "type" : "vec4", + "name" : "in.var.color", + "location" : 2 + }, + { + "type" : "vec3", + "name" : "in.var.norm", + "location" : 3 + } + ], + "outputs" : [ + { + "type" : "vec2", + "name" : "out.var.TEXCOORD0", + "location" : 0 + }, + { + "type" : "vec4", + "name" : "out.var.COLOR0", + "location" : 1 + }, + { + "type" : "vec3", + "name" : "out.var.NORMAL", + "location" : 2 + }, + { + "type" : "vec3", + "name" : "out.var.COLOR1", + "location" : 3 + } + ], + "ubos" : [ + { + "type" : "_12", + "name" : "type.TransformBuffer", + "block_size" : 316, + "set" : 1, + "binding" : 0 + } + ] +} \ No newline at end of file diff --git a/shaders/reflection/model_lit.frag.json b/shaders/reflection/model_lit.frag.json new file mode 100644 index 00000000..ba8372ab --- /dev/null +++ b/shaders/reflection/model_lit.frag.json @@ -0,0 +1,74 @@ +{ + "entryPoints" : [ + { + "name" : "main", + "mode" : "frag" + } + ], + "types" : { + "_6" : { + "name" : "type.LightBuffer", + "members" : [ + { + "name" : "uDiffuseColor", + "type" : "vec4", + "offset" : 0 + }, + { + "name" : "uLightDirection", + "type" : "vec3", + "offset" : 16 + }, + { + "name" : "uLightColor", + "type" : "vec4", + "offset" : 32 + } + ] + } + }, + "inputs" : [ + { + "type" : "vec2", + "name" : "in.var.TEXCOORD0", + "location" : 0 + }, + { + "type" : "vec3", + "name" : "in.var.NORMAL", + "location" : 1 + } + ], + "outputs" : [ + { + "type" : "vec4", + "name" : "out.var.SV_TARGET", + "location" : 0 + } + ], + "separate_images" : [ + { + "type" : "texture2D", + "name" : "uDiffuseTexture", + "set" : 0, + "binding" : 0 + } + ], + "separate_samplers" : [ + { + "type" : "sampler", + "name" : "samplerState", + "set" : 0, + "binding" : 0 + } + ], + "ubos" : [ + { + "type" : "_6", + "name" : "type.LightBuffer", + "block_size" : 48, + "set" : 1, + "binding" : 2 + } + ] +} \ No newline at end of file diff --git a/shaders/reflection/post.frag.json b/shaders/reflection/post.frag.json new file mode 100644 index 00000000..463f0bde --- /dev/null +++ b/shaders/reflection/post.frag.json @@ -0,0 +1,38 @@ +{ + "entryPoints" : [ + { + "name" : "main", + "mode" : "frag" + } + ], + "inputs" : [ + { + "type" : "vec2", + "name" : "in.var.TEXCOORD0", + "location" : 0 + } + ], + "outputs" : [ + { + "type" : "vec4", + "name" : "out.var.SV_TARGET", + "location" : 0 + } + ], + "separate_images" : [ + { + "type" : "texture2D", + "name" : "diffuse", + "set" : 2, + "binding" : 0 + } + ], + "separate_samplers" : [ + { + "type" : "sampler", + "name" : "smp", + "set" : 2, + "binding" : 0 + } + ] +} \ No newline at end of file diff --git a/shaders/reflection/post.vert.json b/shaders/reflection/post.vert.json new file mode 100644 index 00000000..522fdf37 --- /dev/null +++ b/shaders/reflection/post.vert.json @@ -0,0 +1,111 @@ +{ + "entryPoints" : [ + { + "name" : "main", + "mode" : "vert" + } + ], + "types" : { + "_6" : { + "name" : "type.TransformBuffer", + "members" : [ + { + "name" : "world_to_projection", + "type" : "mat4", + "offset" : 0, + "matrix_stride" : 16, + "row_major" : true + }, + { + "name" : "projection_to_world", + "type" : "mat4", + "offset" : 64, + "matrix_stride" : 16, + "row_major" : true + }, + { + "name" : "world_to_view", + "type" : "mat4", + "offset" : 128, + "matrix_stride" : 16, + "row_major" : true + }, + { + "name" : "view_to_projection", + "type" : "mat4", + "offset" : 192, + "matrix_stride" : 16, + "row_major" : true + }, + { + "name" : "camera_pos_world", + "type" : "vec3", + "offset" : 256 + }, + { + "name" : "viewport_min_z", + "type" : "float", + "offset" : 268 + }, + { + "name" : "camera_dir_world", + "type" : "vec3", + "offset" : 272 + }, + { + "name" : "viewport_max_z", + "type" : "float", + "offset" : 284 + }, + { + "name" : "viewport_size", + "type" : "vec2", + "offset" : 288 + }, + { + "name" : "viewport_offset", + "type" : "vec2", + "offset" : 296 + }, + { + "name" : "render_size", + "type" : "vec2", + "offset" : 304 + }, + { + "name" : "time", + "type" : "float", + "offset" : 312 + } + ] + } + }, + "inputs" : [ + { + "type" : "vec2", + "name" : "in.var.pos", + "location" : 0 + }, + { + "type" : "vec2", + "name" : "in.var.uv", + "location" : 1 + } + ], + "outputs" : [ + { + "type" : "vec2", + "name" : "out.var.TEXCOORD0", + "location" : 0 + } + ], + "ubos" : [ + { + "type" : "_6", + "name" : "type.TransformBuffer", + "block_size" : 316, + "set" : 1, + "binding" : 0 + } + ] +} \ No newline at end of file diff --git a/shaders/reflection/ps1.frag.json b/shaders/reflection/ps1.frag.json new file mode 100644 index 00000000..ce98ed69 --- /dev/null +++ b/shaders/reflection/ps1.frag.json @@ -0,0 +1,66 @@ +{ + "entryPoints" : [ + { + "name" : "main", + "mode" : "frag" + } + ], + "types" : { + "_10" : { + "name" : "type.$Globals", + "members" : [ + { + "name" : "ditherPattern2x2", + "type" : "float", + "array" : [ + 4 + ], + "array_size_is_literal" : [ + true + ], + "offset" : 0, + "array_stride" : 16 + } + ] + } + }, + "inputs" : [ + { + "type" : "vec2", + "name" : "in.var.TEXCOORD0", + "location" : 0 + } + ], + "outputs" : [ + { + "type" : "vec4", + "name" : "out.var.SV_TARGET", + "location" : 0 + } + ], + "separate_images" : [ + { + "type" : "texture2D", + "name" : "diffuseTexture", + "set" : 2, + "binding" : 0 + } + ], + "separate_samplers" : [ + { + "type" : "sampler", + "name" : "smp", + "set" : 2, + "binding" : 0 + } + ], + "ubos" : [ + { + "type" : "_10", + "name" : "type.$Globals", + "block_size" : 64, + "set" : 0, + "binding" : 0 + } + ] +} \ No newline at end of file diff --git a/shaders/reflection/ps1.vert.json b/shaders/reflection/ps1.vert.json new file mode 100644 index 00000000..1eac9000 --- /dev/null +++ b/shaders/reflection/ps1.vert.json @@ -0,0 +1,145 @@ +{ + "entryPoints" : [ + { + "name" : "main", + "mode" : "vert" + } + ], + "types" : { + "_8" : { + "name" : "type.TransformBuffer", + "members" : [ + { + "name" : "world_to_projection", + "type" : "mat4", + "offset" : 0, + "matrix_stride" : 16, + "row_major" : true + }, + { + "name" : "projection_to_world", + "type" : "mat4", + "offset" : 64, + "matrix_stride" : 16, + "row_major" : true + }, + { + "name" : "world_to_view", + "type" : "mat4", + "offset" : 128, + "matrix_stride" : 16, + "row_major" : true + }, + { + "name" : "view_to_projection", + "type" : "mat4", + "offset" : 192, + "matrix_stride" : 16, + "row_major" : true + }, + { + "name" : "camera_pos_world", + "type" : "vec3", + "offset" : 256 + }, + { + "name" : "viewport_min_z", + "type" : "float", + "offset" : 268 + }, + { + "name" : "camera_dir_world", + "type" : "vec3", + "offset" : 272 + }, + { + "name" : "viewport_max_z", + "type" : "float", + "offset" : 284 + }, + { + "name" : "viewport_size", + "type" : "vec2", + "offset" : 288 + }, + { + "name" : "viewport_offset", + "type" : "vec2", + "offset" : 296 + }, + { + "name" : "render_size", + "type" : "vec2", + "offset" : 304 + }, + { + "name" : "time", + "type" : "float", + "offset" : 312 + } + ] + }, + "_10" : { + "name" : "type.model", + "members" : [ + { + "name" : "model", + "type" : "mat4", + "offset" : 0, + "matrix_stride" : 16, + "row_major" : true + }, + { + "name" : "color", + "type" : "vec4", + "offset" : 64 + } + ] + } + }, + "inputs" : [ + { + "type" : "vec2", + "name" : "in.var.pos", + "location" : 0 + }, + { + "type" : "vec2", + "name" : "in.var.uv", + "location" : 1 + }, + { + "type" : "vec4", + "name" : "in.var.color", + "location" : 2 + } + ], + "outputs" : [ + { + "type" : "vec2", + "name" : "out.var.TEXCOORD0", + "location" : 0 + }, + { + "type" : "vec4", + "name" : "out.var.COLOR0", + "location" : 1 + } + ], + "ubos" : [ + { + "type" : "_8", + "name" : "type.TransformBuffer", + "block_size" : 316, + "set" : 1, + "binding" : 0 + }, + { + "type" : "_10", + "name" : "type.model", + "block_size" : 80, + "set" : 1, + "binding" : 1 + } + ] +} \ No newline at end of file diff --git a/shaders/reflection/rectangle.frag.json b/shaders/reflection/rectangle.frag.json new file mode 100644 index 00000000..1dbcc5ca --- /dev/null +++ b/shaders/reflection/rectangle.frag.json @@ -0,0 +1,22 @@ +{ + "entryPoints" : [ + { + "name" : "main", + "mode" : "frag" + } + ], + "inputs" : [ + { + "type" : "vec4", + "name" : "in.var.COLOR0", + "location" : 1 + } + ], + "outputs" : [ + { + "type" : "vec4", + "name" : "out.var.SV_TARGET", + "location" : 0 + } + ] +} \ No newline at end of file diff --git a/shaders/reflection/sprite.frag.json b/shaders/reflection/sprite.frag.json new file mode 100644 index 00000000..b7fbb15c --- /dev/null +++ b/shaders/reflection/sprite.frag.json @@ -0,0 +1,43 @@ +{ + "entryPoints" : [ + { + "name" : "main", + "mode" : "frag" + } + ], + "inputs" : [ + { + "type" : "vec2", + "name" : "in.var.TEXCOORD0", + "location" : 0 + }, + { + "type" : "vec4", + "name" : "in.var.COLOR0", + "location" : 1 + } + ], + "outputs" : [ + { + "type" : "vec4", + "name" : "out.var.SV_TARGET", + "location" : 0 + } + ], + "separate_images" : [ + { + "type" : "texture2D", + "name" : "diffuse", + "set" : 2, + "binding" : 0 + } + ], + "separate_samplers" : [ + { + "type" : "sampler", + "name" : "smp", + "set" : 2, + "binding" : 0 + } + ] +} \ No newline at end of file diff --git a/shaders/reflection/sprite.vert.json b/shaders/reflection/sprite.vert.json new file mode 100644 index 00000000..1eac9000 --- /dev/null +++ b/shaders/reflection/sprite.vert.json @@ -0,0 +1,145 @@ +{ + "entryPoints" : [ + { + "name" : "main", + "mode" : "vert" + } + ], + "types" : { + "_8" : { + "name" : "type.TransformBuffer", + "members" : [ + { + "name" : "world_to_projection", + "type" : "mat4", + "offset" : 0, + "matrix_stride" : 16, + "row_major" : true + }, + { + "name" : "projection_to_world", + "type" : "mat4", + "offset" : 64, + "matrix_stride" : 16, + "row_major" : true + }, + { + "name" : "world_to_view", + "type" : "mat4", + "offset" : 128, + "matrix_stride" : 16, + "row_major" : true + }, + { + "name" : "view_to_projection", + "type" : "mat4", + "offset" : 192, + "matrix_stride" : 16, + "row_major" : true + }, + { + "name" : "camera_pos_world", + "type" : "vec3", + "offset" : 256 + }, + { + "name" : "viewport_min_z", + "type" : "float", + "offset" : 268 + }, + { + "name" : "camera_dir_world", + "type" : "vec3", + "offset" : 272 + }, + { + "name" : "viewport_max_z", + "type" : "float", + "offset" : 284 + }, + { + "name" : "viewport_size", + "type" : "vec2", + "offset" : 288 + }, + { + "name" : "viewport_offset", + "type" : "vec2", + "offset" : 296 + }, + { + "name" : "render_size", + "type" : "vec2", + "offset" : 304 + }, + { + "name" : "time", + "type" : "float", + "offset" : 312 + } + ] + }, + "_10" : { + "name" : "type.model", + "members" : [ + { + "name" : "model", + "type" : "mat4", + "offset" : 0, + "matrix_stride" : 16, + "row_major" : true + }, + { + "name" : "color", + "type" : "vec4", + "offset" : 64 + } + ] + } + }, + "inputs" : [ + { + "type" : "vec2", + "name" : "in.var.pos", + "location" : 0 + }, + { + "type" : "vec2", + "name" : "in.var.uv", + "location" : 1 + }, + { + "type" : "vec4", + "name" : "in.var.color", + "location" : 2 + } + ], + "outputs" : [ + { + "type" : "vec2", + "name" : "out.var.TEXCOORD0", + "location" : 0 + }, + { + "type" : "vec4", + "name" : "out.var.COLOR0", + "location" : 1 + } + ], + "ubos" : [ + { + "type" : "_8", + "name" : "type.TransformBuffer", + "block_size" : 316, + "set" : 1, + "binding" : 0 + }, + { + "type" : "_10", + "name" : "type.model", + "block_size" : 80, + "set" : 1, + "binding" : 1 + } + ] +} \ No newline at end of file diff --git a/shaders/sprite.frag.hlsl b/shaders/sprite.frag.hlsl new file mode 100644 index 00000000..0d32a7a8 --- /dev/null +++ b/shaders/sprite.frag.hlsl @@ -0,0 +1,12 @@ +#include "common/pixel.hlsl" + +Texture2D diffuse : register(t0, space2); +SamplerState smp : register(s0, space2); + +// Pixel shader main function +float4 main(PSInput input) : SV_TARGET +{ + float4 color = diffuse.Sample(smp, input.uv); + color *= input.color; + return color; +} \ No newline at end of file diff --git a/shaders/sprite.vert.hlsl b/shaders/sprite.vert.hlsl new file mode 100644 index 00000000..a582f1bf --- /dev/null +++ b/shaders/sprite.vert.hlsl @@ -0,0 +1,2 @@ +#include "common/vertex.hlsl" + diff --git a/shaders/spv/dbgline.frag.spv b/shaders/spv/dbgline.frag.spv new file mode 100644 index 00000000..15bc58d8 Binary files /dev/null and b/shaders/spv/dbgline.frag.spv differ diff --git a/shaders/spv/dbgline.vert.spv b/shaders/spv/dbgline.vert.spv new file mode 100644 index 00000000..534b4622 Binary files /dev/null and b/shaders/spv/dbgline.vert.spv differ diff --git a/shaders/spv/model.frag.spv b/shaders/spv/model.frag.spv new file mode 100644 index 00000000..6e83e47f Binary files /dev/null and b/shaders/spv/model.frag.spv differ diff --git a/shaders/spv/model.vert.spv b/shaders/spv/model.vert.spv new file mode 100644 index 00000000..0ca3365b Binary files /dev/null and b/shaders/spv/model.vert.spv differ diff --git a/shaders/spv/model_lit.frag.spv b/shaders/spv/model_lit.frag.spv new file mode 100644 index 00000000..facbc5ba Binary files /dev/null and b/shaders/spv/model_lit.frag.spv differ diff --git a/shaders/spv/post.frag.spv b/shaders/spv/post.frag.spv new file mode 100644 index 00000000..5f2a8b3a Binary files /dev/null and b/shaders/spv/post.frag.spv differ diff --git a/shaders/spv/post.vert.spv b/shaders/spv/post.vert.spv new file mode 100644 index 00000000..8eead9d6 Binary files /dev/null and b/shaders/spv/post.vert.spv differ diff --git a/shaders/spv/ps1.frag.spv b/shaders/spv/ps1.frag.spv new file mode 100644 index 00000000..b6da6f95 Binary files /dev/null and b/shaders/spv/ps1.frag.spv differ diff --git a/shaders/spv/ps1.vert.spv b/shaders/spv/ps1.vert.spv new file mode 100644 index 00000000..0da79203 Binary files /dev/null and b/shaders/spv/ps1.vert.spv differ diff --git a/shaders/spv/rectangle.frag.spv b/shaders/spv/rectangle.frag.spv new file mode 100644 index 00000000..87ecffe0 Binary files /dev/null and b/shaders/spv/rectangle.frag.spv differ diff --git a/shaders/spv/sprite.frag.spv b/shaders/spv/sprite.frag.spv new file mode 100644 index 00000000..4eff762d Binary files /dev/null and b/shaders/spv/sprite.frag.spv differ diff --git a/shaders/spv/sprite.vert.spv b/shaders/spv/sprite.vert.spv new file mode 100644 index 00000000..0da79203 Binary files /dev/null and b/shaders/spv/sprite.vert.spv differ diff --git a/shaders/wind.hlsl b/shaders/wind.hlsl new file mode 100644 index 00000000..10822523 --- /dev/null +++ b/shaders/wind.hlsl @@ -0,0 +1,6 @@ +#define WIND + +struct Wind +{ + float3 windy : TEXCOORD1; +}; \ No newline at end of file diff --git a/sound.cm b/sound.cm new file mode 100644 index 00000000..2a7e17de --- /dev/null +++ b/sound.cm @@ -0,0 +1,66 @@ +var soloud = use('soloud') +var tween = use('tween') +var io = use('io') +var res = use('resources') +var doc = use('doc') + +soloud.init() + +var audio = {} +var pcms = {} + +// keep every live voice here so GC can’t collect it prematurely +var voices = [] + +// load-and-cache WAVs +audio.pcm = function pcm(file) { + file = res.find_sound(file); if (!file) return + if (pcms[file]) return pcms[file] + var buf = io.slurpbytes(file) + return pcms[file] = soloud.load_wav_mem(buf) +} + +function cleanup() { + var cleaned_voices = voices.filter(v => !v.is_valid()) + cleaned_voices.forEach(v => v.finish_hook?.()) + voices = voices.filter(v => v.is_valid()) +} + +// play a one‑shot; returns the voice for volume/stop control +audio.play = function play(file) { + var pcm = audio.pcm(file); if (!pcm) return + var voice = soloud.play(pcm) + voices.push(voice) + return voice +} + +// cry is just a play+stop closure +audio.cry = function cry(file) { + var v = audio.play(file); if (!v) return + return function() { v.stop(); v = null } +} + +// +// pump + periodic cleanup +// +var ss = use('sdl_audio') +var feeder = ss.open_stream("playback") +feeder.set_format({format:"f32", channels:2, samplerate:44100}) +feeder.resume() + +var FRAMES = 1024 +var CHANNELS = 2 +var BYTES_PER_F = 4 +var CHUNK_BYTES = FRAMES * CHANNELS * BYTES_PER_F + +function pump() { + if (feeder.queued() < CHUNK_BYTES*3) { + feeder.put(soloud.mix(FRAMES)) + cleanup() + } + $_.delay(pump, 1/240) +} + +pump() + +return audio diff --git a/spline.cm b/spline.cm new file mode 100644 index 00000000..524e63c5 --- /dev/null +++ b/spline.cm @@ -0,0 +1,6 @@ +var spline = this + +spline.catmull[cell.DOC] = "Perform Catmull-Rom spline sampling on an array of 2D points, returning an array of samples." +spline.bezier[cell.DOC] = "Perform a Bezier spline (or catmull) sampling on 2D points, returning an array of sampled points." + +return spline \ No newline at end of file diff --git a/tests/animation.ce b/tests/animation.ce new file mode 100644 index 00000000..14e7275c --- /dev/null +++ b/tests/animation.ce @@ -0,0 +1,104 @@ +/* anim.js – drop this at top of your script or in a module */ +var Anim = (() => { + def DEFAULT_MIN = 1 / 60; /* 16 ms – one frame */ + + function play(source, loop=true){ + return { + src : source, + idx : 0, + timer : 0, + loop : loop ?? source.loop ?? true + }; + } + + function update(a, dt){ + a.timer += dt; + def frames = a.src.frames; + while(true){ + def time = Math.max(frames[a.idx].time || 0, Anim.minDelay); + if(a.timer < time) break; /* still on current frame */ + + a.timer -= time; + a.idx += 1; + + if(a.idx >= frames.length){ + if(a.loop) a.idx = 0; + else { a.idx = frames.length - 1; a.timer = 0; break; } + } + } + } + + function current(a){ return a.src.frames[a.idx].image; } + function updateAll(arr, dt){ for(def a of arr) update(a, dt); } + function draw(a, pos, opt, pipe){ + draw2d.image(current(a), pos, 0, [0,0], [0,0], opt, pipe); + } + + return {play, update, current, updateAll, draw, minDelay:DEFAULT_MIN}; +})(); + +var render = use('render'); +/* ── init window ───────────────────────── */ +render.initialize({width:500, height:500, resolution_x:500, resolution_y:500, + mode:'letterboxed', refresh:60}); + +var os = use('os'); +var draw2d = use('draw2d'); +var gfx = use('graphics'); +var transform = use('transform'); + +var camera = { + size: [500,500], + transform: new transform, + fov:50, + near_z: 0, + far_z: 1000, + surface: null, + viewport: {x:0,y:0,width:1,height:1}, + ortho:true, + anchor:[0,0], +} + +/* ── load animations ───────────────────── */ +def crab = gfx.texture('tests/crab'); // gif → Animation +def warrior = gfx.texture('tests/warrior'); // ase → {Original:Animation} + +def anims = [ + Anim.play(crab), // crab.frames + Anim.play(warrior.Run) // warrior.Original.frames +]; + +/* ── fps probe vars ───────────────────── */ +var fpsTimer=0, fpsCount=0 + +Anim.minDelay = 1 / 100; // 10 ms, feel free to tune later + +let last = os.now(); + +function loop(){ + def now = os.now(); + def dt = now - last; // real frame time + last = now; + + Anim.updateAll(anims, dt); + + /* draw */ + render.clear([22/255,120/255,194/255,255/255]); + render.camera(camera); + Anim.draw(anims[0], [ 50,200]); + Anim.draw(anims[1], [250,200]); + render.present(); + + /* fps probe (unchanged) */ + fpsTimer += dt; fpsCount++; + if(fpsTimer >= 0.5){ + prosperon.window.title = + `Anim demo FPS ${(fpsCount/fpsTimer).toFixed(1)}`; + fpsTimer = fpsCount = 0; + } + + /* schedule next tick: aim for 60 Hz but won’t matter to anim speed */ + $_.delay(loop, Math.max(0, (1/60) - (os.now()-now))); +} +loop(); + diff --git a/tests/bunny.png b/tests/bunny.png new file mode 100644 index 00000000..79c31675 Binary files /dev/null and b/tests/bunny.png differ diff --git a/tests/bunnymark.ce b/tests/bunnymark.ce new file mode 100644 index 00000000..69698968 --- /dev/null +++ b/tests/bunnymark.ce @@ -0,0 +1,104 @@ +// bunnymark +var render = use('render') +var os = use('os') +var transform = use('transform') +var dim = [500,500] +render.initialize({ + width:dim.x, + height:dim.y, + resolution_x:dim.x, + resolution_y:dim.y, + mode:"letterboxed", + refresh: 60, +}) + +var camera = { + size: [500,500], + transform: new transform, + fov:50, + near_z: 0, + far_z: 1000, + surface: null, + viewport: {x:0,y:0,width:1,height:1}, + ortho:true, + anchor:[0,0], +} + +var draw = use('draw2d') +var sprite = use('lcdsprite') +var graphics = use('graphics') + +var bunny = graphics.texture('tests/bunny') + +var center = [0.5,0.5] + +var vel = 50 + +function hsl_to_rgb(h, s, l) { + var c = (1 - Math.abs(2 * l - 1)) * s + var x = c * (1 - Math.abs((h / 60) % 2 - 1)) + var m = l - c / 2 + var r = 0, g = 0, b = 0 + + if (h < 60) { r = c; g = x } + else if (h < 120) { r = x; g = c } + else if (h < 180) { g = c; b = x } + else if (h < 240) { g = x; b = c } + else if (h < 300) { r = x; b = c } + else { r = c; b = x } + + return [r + m, g + m, b + m, 1] // 0‒1 floats, alpha = 1 +} + +var bunny_count = 20 + +for (var i = 0; i < bunny_count; i++) { + var pct = i/bunny_count + var hue = 270 * i / bunny_count + var sp = sprite.create(bunny, { + pos: [Math.random()*dim.x*pct, Math.random()*dim.y*pct], + center, + color: hsl_to_rgb(hue, 0.5, 0.5) + }) + sp.dir = [Math.random()*vel, Math.random()*vel] +} + +var dt = 0 +var fps_samples = [] +var fps_window = 10 +var fps_update_period = 0.5 +var last_fps_update = os.now() +function loop() +{ + var now = os.now() + render.clear([22/255,120/255,194/255,255/255]) + render.camera(camera) + + sprite.forEach(x => x.move(x.dir.scale(dt))) + var queue = sprite.queue() + + //log.console(json.encode(queue)) + + for (var q of queue) { + if (!q.image) continue + render.geometry(q.image.texture, q.mesh) + } + + render.present() + dt = os.now() - now + + fps_samples.push(dt) + if (fps_samples.length > fps_window) fps_samples.shift() + + if (now - last_fps_update >= fps_update_period) { + var sum = 0 + for (var i = 0; i < fps_samples.length; i++) sum += fps_samples[i] + prosperon.window.title = `Bunnymark [fps: ${(fps_samples.length/sum).toFixed(1)}]`; + last_fps_update = now + } + + var delay = (1/60) - dt + $_.delay(loop, delay) +} + +loop() diff --git a/tests/camera.ce b/tests/camera.ce new file mode 100644 index 00000000..359555b7 --- /dev/null +++ b/tests/camera.ce @@ -0,0 +1,88 @@ +var render = use('render') +var os = use('os') +var transform = use('transform') +var color = use('color') + +render.initialize({ + width:500, + height:500, + resolution_x:500, + resolution_y:500, + mode: "letterboxed" +}) + +var draw = use('draw2d') + +var camera = { + size: [500,500], + transform: new transform, + fov:50, + near_z: 0, + far_z: 1000, + surface: null, + viewport: {x:0,y:0,width:1,height:1}, + ortho:true, + anchor:[0.5,0.5], +} + +var hudcam = { + size: [500,500], + transform: new transform, + fov:50, + near_z: 0, + far_z: 1000, + surface: null, + viewport: {x:0,y:0,width:1,height:1}, + ortho:true, + anchor:[0,0], +} + +var angle = 0 +var pos = [0,0,0] + +var dt = 0 + +var sprite = use('lcdsprite') +sprite.create("ok", [50,50], [0.5,0]) +sprite.create("nope", [100,100], [0.5,0]) +sprite.create("sad", [150,150], [0.5,0]) + +function loop() +{ + var now = os.now() + pos.x += dt*100 + camera.transform.pos = pos + render.clear([22/255,120/255,194/255,255/255]) + render.camera(camera) + + for (var sp of sprite.sprites) + draw.image(sp.image, sp.rect) + +/* draw.line([[0,0],[100,50]]) + draw.point([100,100]) + draw.circle([200,200],40) + draw.ellipse([300,300],[20,40], {start:0,end:1, thickness:0}) + draw.ellipse([350,350], [30,30], {start:0.1,end:-0.1, thickness:30, color: color.yellow}) + draw.ellipse([100,80],[40,25], {thickness:10, color:color.green}) + draw.ellipse([100,80], [40,25], {thickness:1,color:color.blue}) + draw.rectangle({x:150,y:150,width:50,height:50}) + draw.rectangle({x:100, y:60, width:200, height:60}, {radius: 20, thickness:-3}) + draw.rectangle({x:350, y:60, width:200, height:120}, {radius:10,thickness:3}) +*/ + render.camera(hudcam) + draw.slice9("button_grey", {x:0,y:0,width:100,height:50}, 10) + render.present() + dt = os.now()-now + var delay = (1/240) - dt + if (delay <= 0) + loop() + else + $_.delay(loop, delay) +} + +var sound = use('sound') +//prosperon.myguy = sound.play('test.mp3') + +loop() + + diff --git a/tests/camera_colorspace.ce b/tests/camera_colorspace.ce new file mode 100644 index 00000000..15eaa40b --- /dev/null +++ b/tests/camera_colorspace.ce @@ -0,0 +1,72 @@ +// Test camera colorspace functionality +var camera = use('camera'); +var json = use('json'); + +// Get list of cameras +var cameras = camera.list(); +if (cameras.length == 0) { + log.console("No cameras found!"); + $_. stop(); +} + +var cam_id = cameras[0]; +log.console("Testing camera:", camera.name(cam_id)); + +// Get supported formats +var formats = camera.supported_formats(cam_id); +log.console("\nLooking for different colorspaces in supported formats..."); + +// Group formats by colorspace +var colorspaces = {}; +for (var i = 0; i < formats.length; i++) { + var fmt = formats[i]; + if (!colorspaces[fmt.colorspace]) { + colorspaces[fmt.colorspace] = []; + } + colorspaces[fmt.colorspace].push(fmt); +} + +log.console("\nFound colorspaces:"); +for (var cs in colorspaces) { + log.console(" " + cs + ": " + colorspaces[cs].length + " formats"); +} + +// Try opening camera with different colorspaces +log.console("\nTrying to open camera with different colorspaces..."); + +for (var cs in colorspaces) { + // Get first format for this colorspace + var format = colorspaces[cs][0]; + + log.console("\nTrying colorspace '" + cs + "' with format:"); + log.console(" Resolution: " + format.width + "x" + format.height); + log.console(" Pixel format: " + format.format); + + // You can also create a custom format with a specific colorspace + var custom_format = { + format: format.format, + colorspace: cs, // This will be converted from string + width: format.width, + height: format.height, + framerate_numerator: format.framerate_numerator, + framerate_denominator: format.framerate_denominator + }; + + var cam = camera.open(cam_id, custom_format); + if (cam) { + var actual = cam.get_format(); + log.console(" Opened successfully!"); + log.console(" Actual colorspace: " + actual.colorspace); + + // Camera will be closed when object is freed + cam = null; + } else { + log.console(" Failed to open with this colorspace"); + } + + // Just test first 3 colorspaces + if (Object.keys(colorspaces).indexOf(cs) >= 2) break; +} + +log.console("\nColorspace test complete!"); +$_.stop(); \ No newline at end of file diff --git a/tests/camera_colorspace_convert.ce b/tests/camera_colorspace_convert.ce new file mode 100644 index 00000000..5289a943 --- /dev/null +++ b/tests/camera_colorspace_convert.ce @@ -0,0 +1,106 @@ +// Test camera capture with colorspace conversion +var camera = use('camera'); +var surface = use('surface'); +var json = use('json'); + +// Get first camera +var cameras = camera.list(); +if (cameras.length == 0) { + log.console("No cameras found!"); + $_.stop(); +} + +var cam_id = cameras[0]; +log.console("Using camera:", camera.name(cam_id)); + +// Open camera with default settings +var cam = camera.open(cam_id); +if (!cam) { + log.console("Failed to open camera!"); + $_.stop(); +} + +// Get the format being used +var format = cam.get_format(); +log.console("\nCamera format:"); +log.console(" Resolution:", format.width + "x" + format.height); +log.console(" Pixel format:", format.format); +log.console(" Colorspace:", format.colorspace); + +// Handle camera approval +var approved = false; +$_.receiver(e => { + if (e.type == 'camera_device_approved') { + log.console("\nCamera approved!"); + approved = true; + } else if (e.type == 'camera_device_denied') { + log.error("Camera access denied!"); + $_.stop(); + } +}); + +// Wait for approval then capture +function capture_test() { + if (!approved) { + $_.delay(capture_test, 0.1); + return; + } + + log.console("\nCapturing frame..."); + var surf = cam.capture(); + + if (!surf) { + log.console("No frame captured yet, retrying..."); + $_.delay(capture_test, 0.1); + return; + } + + log.console("\nCaptured surface:"); + log.console(" Size:", surf.width + "x" + surf.height); + log.console(" Format:", surf.format); + + // Test various colorspace conversions + log.console("\nTesting colorspace conversions:"); + + // Convert to sRGB if not already + if (format.colorspace != "srgb") { + try { + var srgb_surf = surf.convert(surf.format, "srgb"); + log.console(" Converted to sRGB colorspace"); + } catch(e) { + log.console(" sRGB conversion failed:", e.message); + } + } + + // Convert to linear sRGB for processing + try { + var linear_surf = surf.convert("rgba8888", "srgb_linear"); + log.console(" Converted to linear sRGB (RGBA8888) for processing"); + } catch(e) { + log.console(" Linear sRGB conversion failed:", e.message); + } + + // Convert to JPEG colorspace (common for compression) + try { + var jpeg_surf = surf.convert("rgb888", "jpeg"); + log.console(" Converted to JPEG colorspace (RGB888) for compression"); + } catch(e) { + log.console(" JPEG colorspace conversion failed:", e.message); + } + + // If YUV format, try BT.709 (HD video standard) + if (surf.format.indexOf("yuv") != -1 || surf.format.indexOf("yuy") != -1) { + try { + var hd_surf = surf.convert(surf.format, "bt709_limited"); + log.console(" Converted to BT.709 limited (HD video standard)"); + } catch(e) { + log.console(" BT.709 conversion failed:", e.message); + } + } + + log.console("\nTest complete!"); + $_.stop(); +} + +// Start capture test after a short delay +$_.delay(capture_test, 0.5); \ No newline at end of file diff --git a/tests/camera_info.ce b/tests/camera_info.ce new file mode 100644 index 00000000..f5d0a316 --- /dev/null +++ b/tests/camera_info.ce @@ -0,0 +1,75 @@ +// Test the new camera functionality +var camera = use('camera'); + +// Get camera drivers +var drivers = camera.drivers(); +log.console("Available camera drivers:", drivers); + +// Get list of cameras +var cameras = camera.list(); +log.console("Found", cameras.length, "cameras"); + +// Get info about each camera +for (var i = 0; i < cameras.length; i++) { + var cam_id = cameras[i]; + log.console("\nCamera", i + 1, "ID:", cam_id); + log.console(" Name:", camera.name(cam_id)); + log.console(" Position:", camera.position(cam_id)); + + // Get supported formats + var formats = camera.supported_formats(cam_id); + log.console(" Supported formats:", formats.length); + + // Show first few formats + for (var j = 0; j < formats.length; j++) { + var fmt = formats[j]; + log.console(" Format", j + 1 + ":"); + log.console(" Pixel format:", fmt.format); + log.console(" Resolution:", fmt.width + "x" + fmt.height); + log.console(" FPS:", fmt.framerate_numerator + "/" + fmt.framerate_denominator, + "(" + (fmt.framerate_numerator / fmt.framerate_denominator) + ")"); + log.console(" Colorspace:", fmt.colorspace); + } +} + +// Open the first camera with a specific format if available +if (cameras.length > 0) { + log.console("\nOpening first camera..."); + var cam_id = cameras[0]; + var formats = camera.supported_formats(cam_id); + + // Try to find a 640x480 format + var preferred_format = null; + for (var i = 0; i < formats.length; i++) { + if (formats[i].width == 640 && formats[i].height == 480) { + preferred_format = formats[i]; + break; + } + } + + var cam; + if (preferred_format) { + log.console("Opening with 640x480 format..."); + cam = camera.open(cam_id, preferred_format); + } else { + log.console("Opening with default format..."); + cam = camera.open(cam_id); + } + + if (cam) { + log.console("Camera opened successfully!"); + log.console("Driver being used:", cam.get_driver()); + + // Get the actual format being used + var actual_format = cam.get_format(); + log.console("Actual format being used:"); + log.console(" Pixel format:", actual_format.format); + log.console(" Resolution:", actual_format.width + "x" + actual_format.height); + log.console(" FPS:", actual_format.framerate_numerator + "/" + actual_format.framerate_denominator, + "(" + (actual_format.framerate_numerator / actual_format.framerate_denominator) + ")"); + log.console(" Colorspace:", actual_format.colorspace); + + // Clean up - camera will be closed when object is freed + cam = null; + } +} \ No newline at end of file diff --git a/tests/crab.gif b/tests/crab.gif new file mode 100644 index 00000000..5bbc12b8 Binary files /dev/null and b/tests/crab.gif differ diff --git a/tests/draw2d.ce b/tests/draw2d.ce new file mode 100644 index 00000000..8be36290 --- /dev/null +++ b/tests/draw2d.ce @@ -0,0 +1,232 @@ +// Test draw2d module without moth framework +var draw2d +var graphics +var os = use('os'); +var input = use('input') +input.watch($_) + +// Create SDL video actor +var video_actor = use('sdl_video'); + +var window_id = null; +var renderer_id = null; + +$_.receiver(e => { + log.console(json.encode(e)) +}) + +// Create window +send(video_actor, { + kind: "window", + op: "create", + data: { + title: "Draw2D Test", + width: 800, + height: 600 + } +}, function(response) { + if (response.error) { + log.error("Failed to create window:", response.error); + return; + } + + window_id = response.id; + log.console("Created window with id:", window_id); + + // Create renderer + send(video_actor, { + kind: "window", + op: "makeRenderer", + id: window_id + }, function(response) { + if (response.error) { + log.error("Failed to create renderer:", response.error); + return; + } + + renderer_id = response.id; + log.console("Created renderer with id:", renderer_id); + + // Configure draw2d and graphics + draw2d = use('draw2d', video_actor, renderer_id) + graphics = use('graphics', video_actor, renderer_id) + + // Start drawing after a short delay + $_.delay(start_drawing, 0.1); + }); +}); + +function start_drawing() { + var frame = 0; + var start_time = os.now(); + + // Note: Image loading would be handled by moth in real implementation + + function draw_frame() { + frame++; + var t = os.now() - start_time; + + // Clear the screen with a dark background + send(video_actor, { + kind: "renderer", + id: renderer_id, + op: "set", + prop: "drawColor", + value: [0.1, 0.1, 0.15, 1] + }); + send(video_actor, { + kind: "renderer", + id: renderer_id, + op: "clear" + }); + + // Clear draw2d commands + draw2d.clear(); + + // Draw some rectangles + draw2d.rectangle( + {x: 50, y: 50, width: 100, height: 100}, + {thickness: 0, color: [1, 0, 0, 1]} + ); + + draw2d.rectangle( + {x: 200, y: 50, width: 100, height: 100}, + {thickness: 5, color: [0, 1, 0, 1]} + ); + + draw2d.rectangle( + {x: 350, y: 50, width: 100, height: 100}, + {thickness: 2, color: [0, 0, 1, 1], radius: 20} + ); + + // Draw circles with animation + var radius = 30 + Math.sin(t * 2) * 10; + draw2d.circle( + [100, 250], + radius, + {color: [1, 1, 0, 1], thickness: 0} + ); + + draw2d.circle( + [250, 250], + 40, + {color: [1, 0, 1, 1], thickness: 3} + ); + + // Draw ellipse + draw2d.ellipse( + [400, 250], + [60, 30], + {color: [0, 1, 1, 1], thickness: 2} + ); + + // Draw lines + var line_y = 350 + Math.sin(t * 3) * 20; + draw2d.line( + [[50, line_y], [150, line_y + 50], [250, line_y]], + {color: [1, 0.5, 0, 1], thickness: 2} + ); + + // Draw cross + draw2d.cross( + [350, 375], + 25, + {color: [0.5, 1, 0.5, 1], thickness: 3} + ); + + // Draw arrow + draw2d.arrow( + [450, 350], + [550, 400], + 15, + 30, + {color: [1, 1, 1, 1], thickness: 2} + ); + + // Draw partial circle (arc) + draw2d.circle( + [150, 480], + 50, + { + color: [0.8, 0.8, 1, 1], + thickness: 5, + start: 0.25, + end: 0.75 + } + ); + + // Draw filled partial ellipse + draw2d.ellipse( + [350, 480], + [80, 40], + { + color: [1, 0.8, 0.8, 1], + thickness: 0, + start: 0, + end: 0.6 + } + ); + + // Draw some points in a pattern + var point_count = 20; + for (var i = 0; i < point_count; i++) { + var angle = (i / point_count) * Math.PI * 2; + var r = 30 + Math.sin(t * 4 + i * 0.5) * 10; + var px = 650 + Math.cos(angle) * r; + var py = 300 + Math.sin(angle) * r; + + draw2d.point( + [px, py], + 3, + {}, + {color: [1, 0.5 + Math.sin(t * 2 + i) * 0.5, 0.5, 1]} + ); + } + + var img = "tests/bunny.png" + draw2d.image(img, {x: 500, y: 450, width: 64, height: 64}); + + // Rotating bunny + var rotation = t * 0.5; + draw2d.image( + img, + {x: 600, y: 450, width: 64, height: 64}, + rotation, + [0.5, 0.5] // Center anchor + ); + + // Bouncing bunny with tint + var bounce_y = 500 + Math.sin(t * 3) * 20; + draw2d.image( + img, + {x: 700, y: bounce_y, width: 48, height: 48}, + 0, + [0.5, 1], // Bottom center anchor + [0, 0], // No shear + {color: [1, 0.5, 0.5, 1]} // Red tint + ); + + // Flush all commands to renderer + draw2d.flush(); + + // Present the frame + send(video_actor, { + kind: "renderer", + id: renderer_id, + op: "present" + }); + + // Schedule next frame (60 FPS) + if (frame < 600) { // Run for 10 seconds + $_.delay(draw_frame, 1/60); + } else { + log.console("Test completed - drew", frame, "frames"); + $_.delay($_.stop, 0.5); + } + } + + draw_frame(); +} + +// Stop after 12 seconds if not already stopped +$_.delay($_.stop, 12); \ No newline at end of file diff --git a/tests/prosperon.ce b/tests/prosperon.ce new file mode 100644 index 00000000..ae7dfc6f --- /dev/null +++ b/tests/prosperon.ce @@ -0,0 +1,64 @@ +var draw2d = use('draw2d') +var color = use('color') + +function update(dt) +{ + // Update game logic here + return {} +} + +function draw() +{ + // Clear the draw list + draw2d.clear() + + // Draw a red outlined rectangle + draw2d.rectangle( + {x: 100, y: 100, width: 200, height: 150}, // rect + {thickness: 3}, // geometry options + {color: color.red} // material + ) + + // Draw a filled green circle + draw2d.circle( + [300, 300], // position + 50, // radius + {thickness: 0}, // geometry (0 = filled) + {color: color.green} // material + ) + + // Draw blue rounded rectangle + draw2d.rectangle( + {x: 350, y: 200, width: 150, height: 100}, + {thickness: 2, radius: 15}, // rounded corners + {color: color.blue} + ) + + // Draw text + draw2d.text( + "Hello from Prosperon!", + {x: 50, y: 50}, + 'fonts/c64.ttf', + 16, + color.white, // no wrap + ) + + // Draw a line + draw2d.line( + [[50, 400], [150, 450], [250, 400]], + {thickness: 2}, + {color: color.yellow} + ) + + draw2d.image("tests/bunny", {x:0,y:0}) + + // Return the draw commands + return draw2d.get_commands() +} + +$_.receiver(e => { + if (e.kind == 'update') + send(e, update(e.dt)) + else if (e.kind == 'draw') + send(e, draw()) +}) \ No newline at end of file diff --git a/tests/steam.ce b/tests/steam.ce new file mode 100644 index 00000000..73a72e73 --- /dev/null +++ b/tests/steam.ce @@ -0,0 +1,40 @@ +var steam = use("steam"); + +log.console("Steam module loaded:", steam); + +if (steam) { + log.console("Steam functions available:"); + log.console("- steam_init:", typeof steam.steam_init); + log.console("- steam_shutdown:", typeof steam.steam_shutdown); + log.console("- steam_run_callbacks:", typeof steam.steam_run_callbacks); + + log.console("\nSteam sub-modules:"); + log.console("- stats:", steam.stats); + log.console("- achievement:", steam.achievement); + log.console("- app:", steam.app); + log.console("- user:", steam.user); + log.console("- friends:", steam.friends); + log.console("- cloud:", steam.cloud); + + // Try to initialize Steam + log.console("\nAttempting to initialize Steam..."); + var init_result = steam.steam_init(); + log.console("Initialization result:", init_result); + + if (init_result) { + // Get some basic info + log.console("\nApp ID:", steam.app.app_id()); + log.console("User logged on:", steam.user.user_logged_on()); + + if (steam.user.user_logged_on()) { + log.console("User name:", steam.friends.friends_name()); + log.console("User state:", steam.friends.friends_state()); + } + + // Shutdown when done + steam.steam_shutdown(); + log.console("Steam shut down"); + } +} else { + log.console("Steam module not available (compiled without Steam support)"); +} \ No newline at end of file diff --git a/tests/surface.ce b/tests/surface.ce new file mode 100644 index 00000000..693fb30c --- /dev/null +++ b/tests/surface.ce @@ -0,0 +1,37 @@ +// Test SDL_Surface module +var Surface = use('surface'); + +// Test creating a surface +var surf = new Surface({width: 100, height: 100}); +log.console("Created surface:", surf.width, "x", surf.height); + +log.console(json.encode(surf)) + +// Test fill +surf.fill([1, 0, 0, 1]); // Red + +// Test dup +var surf2 = surf.dup(); +log.console("Duplicated surface:", surf2.width, "x", surf2.height); + +// Test scale +var surf3 = surf.scale([50, 50], "linear"); +log.console("Scaled surface:", surf3.width, "x", surf3.height); + +// Test format +log.console("Surface format:", surf.format); + +// Test pixels +var pixels = surf.pixels(); +log.console("Got pixels array buffer, length:", pixels.byteLength); + +// Test creating surface with custom format +var surf4 = new Surface({width: 64, height: 64, format: "rgb24"}); +log.console("Created RGB24 surface:", surf4.width, "x", surf4.height, "format:", surf4.format); + +// Test creating surface from pixels +var pixelData = new ArrayBuffer(32 * 32 * 4); // 32x32 RGBA +var surf5 = new Surface({width: 32, height: 32, pixels: pixelData}); +log.console("Created surface from pixels:", surf5.width, "x", surf5.height); + +log.console("Surface module test passed!"); \ No newline at end of file diff --git a/tests/surface_colorspace.ce b/tests/surface_colorspace.ce new file mode 100644 index 00000000..ca14f4e3 --- /dev/null +++ b/tests/surface_colorspace.ce @@ -0,0 +1,63 @@ +// Test surface colorspace conversion +var surface = use('surface'); +var json = use('json'); + +// Create a test surface +var surf = surface({ + width: 640, + height: 480, + format: "rgb888" +}); + +log.console("Created surface:"); +log.console(" Size:", surf.width + "x" + surf.height); +log.console(" Format:", surf.format); + +// Fill with a test color +surf.fill([1, 0.5, 0.25, 1]); // Orange color + +// Test 1: Convert format only (no colorspace change) +log.console("\nTest 1: Convert to RGBA8888 format only"); +var converted1 = surf.convert("rgba8888"); +log.console(" New format:", converted1.format); + +// Test 2: Convert format and colorspace +log.console("\nTest 2: Convert to YUY2 format with JPEG colorspace"); +var converted2 = surf.convert("yuy2", "jpeg"); +log.console(" New format:", converted2.format); + +// Test 3: Try different colorspaces +var colorspaces = ["srgb", "srgb_linear", "jpeg", "bt601_limited", "bt709_limited"]; +var test_format = "rgba8888"; + +log.console("\nTest 3: Converting to", test_format, "with different colorspaces:"); +for (var i = 0; i < colorspaces.length; i++) { + try { + var conv = surf.convert(test_format, colorspaces[i]); + log.console(" " + colorspaces[i] + ": Success"); + } catch(e) { + log.console(" " + colorspaces[i] + ": Failed -", e.message); + } +} + +// Test 4: YUV formats with appropriate colorspaces +log.console("\nTest 4: YUV format conversions:"); +var yuv_tests = [ + {format: "yuy2", colorspace: "jpeg"}, + {format: "nv12", colorspace: "bt601_limited"}, + {format: "nv21", colorspace: "bt709_limited"}, + {format: "yvyu", colorspace: "bt601_full"} +]; + +for (var i = 0; i < yuv_tests.length; i++) { + var test = yuv_tests[i]; + try { + var conv = surf.convert(test.format, test.colorspace); + log.console(" " + test.format + " with " + test.colorspace + ": Success"); + } catch(e) { + log.console(" " + test.format + " with " + test.colorspace + ": Failed -", e.message); + } +} + +log.console("\nColorspace conversion test complete!"); +$_.stop(); \ No newline at end of file diff --git a/tests/warrior.aseprite b/tests/warrior.aseprite new file mode 100644 index 00000000..c4964b00 Binary files /dev/null and b/tests/warrior.aseprite differ diff --git a/tests/webcam.ce b/tests/webcam.ce new file mode 100644 index 00000000..2628ad8d --- /dev/null +++ b/tests/webcam.ce @@ -0,0 +1,263 @@ +// Test webcam display +var draw2d +var graphics +var os = use('os'); +var input = use('input') +var json = use('json') +var surface = use('surface') +input.watch($_) + +// Create SDL video actor +var video_actor = use('sdl_video'); +var camera = use('camera') + +var window_id = null; +var renderer_id = null; +var cam_id = null; +var cam_obj = null; +var cam_approved = false; +var webcam_texture = null; + +// Handle camera events +$_.receiver(e => { + if (e.type == 'camera_device_approved' && e.which == cam_id) { + log.console("Camera approved!"); + cam_approved = true; + } else if (e.type == 'camera_device_denied' && e.which == cam_id) { + log.error("Camera access denied!"); + $_.stop(); + } +}) + +// Create window +send(video_actor, { + kind: "window", + op: "create", + data: { + title: "Webcam Test", + width: 800, + height: 600 + } +}, function(response) { + if (response.error) { + log.error("Failed to create window:", response.error); + return; + } + + window_id = response.id; + log.console("Created window with id:", window_id); + + // Create renderer + send(video_actor, { + kind: "window", + op: "makeRenderer", + id: window_id + }, function(response) { + if (response.error) { + log.error("Failed to create renderer:", response.error); + return; + } + + renderer_id = response.id; + log.console("Created renderer with id:", renderer_id); + + // Configure draw2d and graphics + draw2d = use('draw2d', video_actor, renderer_id) + graphics = use('graphics', video_actor, renderer_id) + + // List available cameras + var cameras = camera.list(); + if (cameras.length == 0) { + log.error("No cameras found!"); + log.console(json.encode(cameras)) + $_.stop(); + return; + } + + log.console("Found", cameras.length, "camera(s)"); + + // Open the first camera + cam_id = cameras[0]; + var cam_name = camera.name(cam_id); + var cam_position = camera.position(cam_id); + log.console("Opening camera:", cam_name, "Position:", cam_position); + + // Get supported formats and try to find a good one + var formats = camera.supported_formats(cam_id); + log.console("Camera supports", formats.length, "formats"); + + // Look for a 640x480 format with preferred colorspace + var preferred_format = null; + for (var i = 0; i < formats.length; i++) { + if (formats[i].width == 640 && formats[i].height == 480) { + preferred_format = formats[i]; + // Prefer JPEG or sRGB colorspace if available + if (formats[i].colorspace == "jpeg" || formats[i].colorspace == "srgb") { + break; + } + } + } + + if (!preferred_format && formats.length > 0) { + // Use first available format + preferred_format = formats[0]; + } + + preferred_format.framerate_numerator = 30 + + if (preferred_format) { + log.console("Using format:", preferred_format.width + "x" + preferred_format.height, + "FPS:", preferred_format.framerate_numerator + "/" + preferred_format.framerate_denominator, + "Format:", preferred_format.format, + "Colorspace:", preferred_format.colorspace); + cam_obj = camera.open(cam_id, preferred_format); + } else { + cam_obj = camera.open(cam_id); + } + + if (!cam_obj) { + log.error("Failed to open camera!"); + $_.stop(); + return; + } + + log.console("Camera driver:", cam_obj.get_driver()); + + // Get and display the actual format being used + var actual_format = cam_obj.get_format(); + log.console("Actual camera format:"); + log.console(" Resolution:", actual_format.width + "x" + actual_format.height); + log.console(" Format:", actual_format.format); + log.console(" Colorspace:", actual_format.colorspace); + log.console(" FPS:", actual_format.framerate_numerator + "/" + actual_format.framerate_denominator); + + // Start capturing after a short delay to wait for approval + $_.delay(start_capturing, 0.5); + }); +}); + +var captured = false + +function start_capturing() { + if (!cam_approved) { + log.console("Waiting for camera approval..."); + $_.delay(start_capturing, 0.1); + return; + } + + var frame = 0; + var start_time = os.now(); + + function capture_and_draw() { + frame++; + var t = os.now() - start_time; + + // Clear the screen with a dark background + send(video_actor, { + kind: "renderer", + id: renderer_id, + op: "set", + prop: "drawColor", + value: [0.1, 0.1, 0.15, 1] + }); + + send(video_actor, { + kind: "renderer", + id: renderer_id, + op: "clear" + }); + + // Clear draw2d commands + draw2d.clear(); + + // Capture frame from camera + var surface = cam_obj.capture() + + if (surface) { + // Create texture from surface directly + send(video_actor, { + kind: "renderer", + id: renderer_id, + op: "loadTexture", + data: surface + }, function(tex_response) { + if (tex_response.id) { + // Destroy old texture if exists to avoid memory leak + if (webcam_texture) { + send(video_actor, { + kind: "texture", + id: webcam_texture, + op: "destroy" + }); + } + webcam_texture = tex_response.id; + } + }); + } + + // Draw the webcam texture if we have one + if (webcam_texture) { + send(video_actor, { + kind: "renderer", + id: renderer_id, + op: "copyTexture", + data: { + texture_id: webcam_texture, + dest: {x: 50, y: 50, width: 640, height: 480} + } + }); + } else { + // Draw placeholder text while waiting for first frame +// draw2d.text("Waiting for webcam...", {x: 200, y: 250, size: 20}); + } + + // Draw info + /* + draw2d.text("Camera: " + camera.name(cam_id), {x: 20, y: 20, size: 16}); + draw2d.text("Position: " + camera.position(cam_id), {x: 20, y: 40, size: 16}); + draw2d.text("Frame: " + frame, {x: 20, y: 60, size: 16}); + */ + // Flush all commands to renderer + draw2d.flush(); + + // Present the frame + send(video_actor, { + kind: "renderer", + id: renderer_id, + op: "present" + }); + + // Schedule next frame (30 FPS for webcam) + if (frame < 300) { // Run for 10 seconds + $_.delay(capture_and_draw, 1/30); + } else { + log.console("Test completed - captured", frame, "frames"); + + // Clean up resources + if (webcam_texture) { + send(video_actor, { + kind: "texture", + id: webcam_texture, + op: "destroy" + }); + } + + // Note: Camera is automatically closed when the object is garbage collected + cam_obj = null; + + $_.delay($_.stop, 0.5); + } + } + + capture_and_draw(); +} + +$_.delay(_ => { + // Capture frame from camera + var surface = cam_obj.capture().convert("rgba8888", "srgb") + log.console('capturing!') + graphics.save_png("test.png", surface.width, surface.height, surface.pixels(),surface.pitch) +}, 3) + +// Stop after 12 seconds if not already stopped +$_.delay($_.stop, 12); \ No newline at end of file diff --git a/tests/window.ce b/tests/window.ce new file mode 100644 index 00000000..2d0749fd --- /dev/null +++ b/tests/window.ce @@ -0,0 +1,56 @@ +//var draw = use('draw2d') + +prosperon.win = prosperon.engine_start({ + title:`Prosperon [${prosperon.version}-${prosperon.revision}]`, + width: 1280, + height: 720, + high_dpi:0, + alpha:1, + fullscreen:0, + sample_count:1, + enable_clipboard:true, + enable_dragndrop: true, + max_dropped_files: 1, + swap_interval: 1, + name: "Prosperon", + version:prosperon.version + "-" + prosperon.revision, + identifier: "world.pockle.prosperon", + creator: "Pockle World LLC", + copyright: "Copyright Pockle World 2025", + type: "game", + url: "https://prosperon.dev" +}) + +var ren = prosperon.win.make_renderer("vulkan") + +function loop() { + ren.draw_color([1,1,1,1]) + + ren.clear() + ren.draw_color([0,0,0,1]) + ren.fillrect({x:50,y:50,height:50,width:50}) + ren.present() + $_.delay(loop, 1/60) +} +loop() + +$_.delay($_.stop, 3) + +var os = use('os') +var actor = use('actor') +var ioguy = {} +ioguy[cell.actor_sym] = { + id: actor.ioactor() +} + +send(ioguy, { + type: "subscribe", + actor: $_ +}) + +$_.receiver(e => { + if (e.type == 'quit') + os.exit() + else + log.console(json.encode(e)) +}) diff --git a/tilemap.cm b/tilemap.cm new file mode 100644 index 00000000..daafb4d6 --- /dev/null +++ b/tilemap.cm @@ -0,0 +1,193 @@ +// tilemap + +function tilemap() +{ + this.tiles = []; + this.offset_x = 0; + this.offset_y = 0; + this.layer = 0; // Default layer for scene tree sorting + this._geometry_cache = {}; // Cache actual geometry data by texture + this._dirty = true; + this.scale_x = 1 + this.scale_y = 1 + return this; +} + +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++) { + if (map.tiles[x][y] != null) { + var result = fn(map.tiles[x][y], { + x: x + map.offset_x, + y: y + map.offset_y + }); + if (result != null) { + map.tiles[x][y] = result; + } + } + } + } +} + +tilemap.prototype = +{ + at(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) { + // Shift arrays if negative indices + if (pos.x < this.offset_x) { + var shift = this.offset_x - pos.x; + var new_tiles = []; + for (var i = 0; i < shift; i++) new_tiles[i] = []; + this.tiles = new_tiles.concat(this.tiles); + this.offset_x = pos.x; + } + + if (pos.y < this.offset_y) { + var shift = this.offset_y - pos.y; + for (var i = 0; i < this.tiles.length; i++) { + if (!this.tiles[i]) this.tiles[i] = []; + var new_col = []; + for (var j = 0; j < shift; j++) new_col[j] = null; + this.tiles[i] = new_col.concat(this.tiles[i]); + } + this.offset_y = pos.y; + } + + var x = pos.x - this.offset_x; + var y = pos.y - this.offset_y; + + // Ensure array exists up to x + 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') { + var graphics = use('graphics'); + image = graphics.texture(image); + } + // Note: if image is null, it will remove the tile + + // Set the value (null removes the tile) + this.tiles[x][y] = image; + + // Mark cache as dirty when tiles change + this._dirty = true; + }, + + clear() { + this.tiles = []; + this.offset_x = 0; + this.offset_y = 0; + this._geometry_cache = {}; + this._dirty = true; + }, + + // Build cached geometry grouped by texture + _build_geometry_cache(pos = {x: 0, y: 0}) { + var geometry = use('geometry'); + + // Group tiles by texture (using a unique key per image object) + var textureGroups = {}; + var imageToKey = new Map(); // Map image objects to unique keys + var keyCounter = 0; + + // Collect all tiles and their positions + 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 tile = this.tiles[x][y]; + if (tile) { + if (!imageToKey.has(tile)) { + var key = `texture_${keyCounter++}`; + imageToKey.set(tile, key); + log.console(`New texture key: ${key} for tile ${tile}`) + } + var textureKey = imageToKey.get(tile); + + if (!textureGroups[textureKey]) { + textureGroups[textureKey] = { + tiles: [], + image: tile, // Store the image object + offset_x: this.offset_x, + offset_y: this.offset_y, + size_x: this.scale_x, + size_y: this.scale_y + }; + } + textureGroups[textureKey].tiles.push({ + x: x + this.offset_x, + y: y + this.offset_y, + image: tile + }); + } + } + } + + // Generate and cache geometry for each texture group + this._geometry_cache = {}; + for (var textureKey in textureGroups) { + var group = textureGroups[textureKey]; + if (group.tiles.length == 0) continue; + + // Create a temporary tilemap for this texture group + var tempMap = { + tiles: [], + offset_x: group.offset_x, + offset_y: group.offset_y, + size_x: group.size_x, // now in world-units + size_y: group.size_y, + pos_x: pos.x, + pos_y: pos.y + }; + + // Build sparse array for this texture's tiles + group.tiles.forEach(({x, y, image}) => { + var arrayX = x - group.offset_x; + var arrayY = y - group.offset_y; + if (!tempMap.tiles[arrayX]) tempMap.tiles[arrayX] = []; + tempMap.tiles[arrayX][arrayY] = image; + }); + + // Generate and cache geometry for this group + var geom = geometry.tilemap_to_data(tempMap); + this._geometry_cache[textureKey] = { + geometry: geom, + image: group.image + }; + } + + this._dirty = false; + }, + + draw() { + var pos = this.pos || {x:0,y:0} + // Rebuild cache if dirty or position changed + if (this._dirty || Object.keys(this._geometry_cache).length == 0 || this._last_pos?.x != pos.x || this._last_pos?.y != pos.y) { + this._build_geometry_cache(pos); + this._last_pos = {x: pos.x, y: pos.y}; + } + + // Generate commands from cached geometry, pulling texture_id dynamically + var commands = []; + var i = 0 + for (var textureKey in this._geometry_cache) { + var cached = this._geometry_cache[textureKey]; + commands.push({ + cmd: "geometry", + geometry: cached.geometry, + image: cached.image, + texture_id: cached.image.gpu // Pull GPU ID dynamically on each draw + }); + } + + return commands; + }, +} + +return tilemap \ No newline at end of file diff --git a/tween.cm b/tween.cm new file mode 100644 index 00000000..535d1cde --- /dev/null +++ b/tween.cm @@ -0,0 +1,240 @@ +var Ease = use('ease') +var time = use('time') + +var rate = 1/240 + +var TweenEngine = { + tweens: [], + default_clock: null, // Will be set during init + add(tween) { + this.tweens.push(tween) + }, + remove(tween) { + this.tweens = this.tweens.filter(t => t != tween) + }, + update(current_time) { + // If no time provided, use real time + if (current_time == null) { + current_time = time.number() + } + for (var tween of this.tweens.slice()) { + tween._update(current_time) + } + }, + clear() { + this.tweens = [] + } +} + +function Tween(obj) { + this.obj = obj + this.startVals = {} + this.endVals = {} + this.duration = 0 + this.easing = Ease.linear + this.startTime = 0 + this.onCompleteCallback = function() {} + this.onUpdateCallback = null + this.engine = null // Track which engine owns this tween +} + +Tween.prototype.to = function(props, duration, start_time) { + for (var key in props) { + var value = props[key] + if (typeof value == 'object' && value != null && !Array.isArray(value)) { + // Handle nested objects by flattening them + for (var subkey in value) { + var flatKey = key + '.' + subkey + this.startVals[flatKey] = this.obj[key] ? this.obj[key][subkey] : undefined + this.endVals[flatKey] = value[subkey] + } + } else { + this.startVals[key] = this.obj[key] + this.endVals[key] = value + } + } + this.duration = duration + + // If no start_time provided, use the default clock + if (start_time == null) { + this.startTime = TweenEngine.default_clock ? TweenEngine.default_clock() : time.number() + } else { + this.startTime = start_time + } + + this.engine = this.engine || TweenEngine + this.engine.add(this) + return this +} + +Tween.prototype.ease = function(easingFn) { + this.easing = easingFn + return this +} + +Tween.prototype.onComplete = function(callback) { + this.onCompleteCallback = callback + return this +} + +Tween.prototype.onUpdate = function(cb) { + this.onUpdateCallback = cb + return this +} + +Tween.prototype._update = function(now) { + this.seek(now) + this.onUpdateCallback?.() +} + +Tween.prototype.seek = function(global_time) { + var elapsed = global_time - this.startTime + var t = Math.min(Math.max(elapsed / this.duration, 0), 1) + var eased = this.easing(t) + + for (var key in this.endVals) { + var start = this.startVals[key] + var end = this.endVals[key] + var value = start + (end - start) * eased + + if (key.includes('.')) { + // Handle nested object properties + var parts = key.split('.') + var objKey = parts[0] + var subKey = parts[1] + + // Ensure the nested object exists + if (!this.obj[objKey]) { + this.obj[objKey] = {} + } + + this.obj[objKey][subKey] = value + } else { + this.obj[key] = value + } + } + + if (t == 1 && this.engine) { + this.onCompleteCallback() + this.engine.remove(this) + } +} + +Tween.prototype.cancel = function() { + if (this.engine) { + this.engine.remove(this) + } +} + +Tween.prototype.toJSON = function() { + return { + startVals: this.startVals, + endVals: this.endVals, + duration: this.duration, + startTime: this.startTime, + easing: this.easing.name || 'linear' + } +} + +function Timeline() { + this.current_time = 0 + this.events = [] // { time, fn, fired } + this.playing = false + this.last_tick = 0 + this.engine = { + tweens: [], + add: TweenEngine.add.bind(this.engine), + remove: TweenEngine.remove.bind(this.engine), + update: TweenEngine.update.bind(this.engine), + clear: TweenEngine.clear.bind(this.engine) + } + this.engine.tweens = [] +} + +Timeline.prototype.add_event = function(time, fn) { + this.events.push({ time, fn, fired: false }) +} + +Timeline.prototype.add_tween = function(obj, props, duration, start_time) { + var tw = new Tween(obj) + tw.engine = this.engine + return tw.to(props, duration, start_time) +} + +Timeline.prototype.play = function() { + this.playing = true + this.last_tick = time.number() + var loop = () => { + if (!this.playing) return + var now = time.number() + var dt = now - this.last_tick + this.last_tick = now + this.current_time += dt + this.seek(this.current_time) + $_.delay(loop, rate) + } + loop() +} + +Timeline.prototype.pause = function() { + this.playing = false +} + +Timeline.prototype.seek = function(t) { + this.current_time = t + // Update all tweens in this timeline + this.engine.update(t) + // Fire any events + for (var ev of this.events) { + if (!ev.fired && t >= ev.time) { + ev.fn() + ev.fired = true + } else if (ev.fired && t < ev.time) { + // Reset fired flag when seeking backwards + ev.fired = false + } + } +} + +Timeline.prototype.toJSON = function() { + return { + current_time: this.current_time, + events: this.events.map(e => ({ time: e.time, fired: e.fired })), + tweens: this.engine.tweens.map(t => t.toJSON()) + } +} + +// Live update loop for fire-and-forget tweens +function live_update_loop() { + TweenEngine.update() + $_.delay(live_update_loop, rate) +} + +// Factory function +function tween(obj, engine) { + var tw = new Tween(obj) + if (engine) { + tw.engine = engine + } + return tw +} + +// Initialize with a default clock that returns real time +function init(default_clock) { + TweenEngine.default_clock = default_clock || (() => time.number()) + // Start the live update loop + live_update_loop() +} + +// Auto-init with real time if not explicitly initialized +$_.delay(() => { + if (!TweenEngine.default_clock) { + init() + } +}, 0) + +tween.init = init +tween.Timeline = Timeline +tween.TweenEngine = TweenEngine + +return tween diff --git a/video.cm b/video.cm new file mode 100644 index 00000000..e4e63464 --- /dev/null +++ b/video.cm @@ -0,0 +1,5 @@ +var video = this + +video.make_video[cell.DOC] = "Decode a video file (MPEG, etc.) from an ArrayBuffer, returning a datastream object." + +return video \ No newline at end of file