From d52d50fe61cccdf220c5b1130fcfad20b9655c6b Mon Sep 17 00:00:00 2001 From: John Alanbrook Date: Sat, 5 Jul 2025 10:24:09 -0500 Subject: [PATCH] fixes for gameplay --- CLAUDE.md | 12 +- prosperon/controller.cm | 1 + prosperon/draw2d.cm | 55 ++++++- prosperon/ease.cm | 124 +++++++++++++++ prosperon/prosperon.ce | 214 ++++++++++++-------------- prosperon/sdl_video.ce | 2 + prosperon/sound.cm | 5 +- prosperon/tween.cm | 332 ++++++++-------------------------------- scripts/engine.cm | 6 +- source/jsffi.c | 42 +++-- source/qjs_sdl_input.c | 46 ++++-- source/qjs_sdl_video.c | 10 +- 12 files changed, 423 insertions(+), 426 deletions(-) create mode 100644 prosperon/ease.cm diff --git a/CLAUDE.md b/CLAUDE.md index 992358fa..b97e8a23 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -16,7 +16,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co After install with 'make', just run 'cell' and point it at the actor you want to launch. "cell tests/toml" runs the actor "tests/toml.js" ## Scripting language -This is called "cell", but it is is a variant of javascript and extremely similar. +This is called "cell", a variant of JavaScript with important differences. See docs/cell.md for detailed language documentation. ### Common development commands - `meson setup build_` - Configure build directory @@ -34,12 +34,16 @@ Prosperon is an actor-based game engine inspired by Douglas Crockford's Misty sy - Hierarchical actor system with spawning/killing - Actor lifecycle: awake, update, draw, garbage collection -### JavaScript Style Guide +### Cell Language Style Guide - Use `use()` function for imports (Misty-style, not ES6 import/export) - Prefer closures and javascript objects and prototypes over ES6 style classes - Follow existing JavaScript patterns in the codebase - Functions as first-class citizens -- Do not use const or let; only var +- Use `def` for constants (not const) +- Use `var` for variables (block-scoped like let) +- Check for null with `== null` (no undefined in Cell) +- Use `==` for equality (always strict, no `===`) +- See docs/cell.md for complete language reference ### Core Systems 1. **Actor System** (scripts/core/engine.js) @@ -99,7 +103,7 @@ cd examples/chess - Documentation is found in docs - Documentation for the JS modules loaded with 'use' is docs/api/modules - .md files directly in docs gives a high level overview -- docs/dull is what this specific Javascript system is (including alterations from quickjs/es6) +- docs/cell.md documents the Cell language (JavaScript variant used in Prosperon) ### Shader Development - Shaders are in `shaders/` directory as HLSL diff --git a/prosperon/controller.cm b/prosperon/controller.cm index d18b6148..f1b8d32d 100644 --- a/prosperon/controller.cm +++ b/prosperon/controller.cm @@ -1,4 +1,5 @@ var input = use('input') +return {} var downkeys = {}; diff --git a/prosperon/draw2d.cm b/prosperon/draw2d.cm index f7555a13..f02347d1 100644 --- a/prosperon/draw2d.cm +++ b/prosperon/draw2d.cm @@ -129,15 +129,15 @@ draw.slice9 = function slice9(image, rect = [0,0], slice = 0, info = slice9_info if (!image) throw Error('Need an image to render.') add_command("draw_slice9", { - image: image, - rect: rect, - slice: slice, - info: info, - material: material + image, + rect, + slice, + info, + material }) } -draw.image = function image(image, rect, rotation, anchor, shear, info, material) { +draw.image = function image(image, rect, rotation, anchor, shear, info = {mode:"nearest"}, material) { if (!rect) throw Error('Need rectangle to render image.') if (!image) throw Error('Need an image to render.') @@ -150,7 +150,7 @@ draw.image = function image(image, rect, rotation, anchor, shear, info, material anchor, shear, info, - material, + material }) } @@ -158,7 +158,7 @@ draw.circle = function render_circle(pos, radius, defl, material) { draw.ellipse(pos, [radius,radius], defl, material) } -draw.text = function text(text, pos, font = 'fonts/c64.ttf', size = 8, color = color.white, wrap = 0) { +draw.text = function text(text, pos, font = 'fonts/c64.ttf', size = 8, color = {r:1,g:1,b:1,a:1}, wrap = 0) { add_command("draw_text", { text, pos, @@ -169,4 +169,43 @@ draw.text = function text(text, pos, font = 'fonts/c64.ttf', size = 8, color = c }) } +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) + } + } +} + return draw \ No newline at end of file diff --git a/prosperon/ease.cm b/prosperon/ease.cm new file mode 100644 index 00000000..a00376e1 --- /dev/null +++ b/prosperon/ease.cm @@ -0,0 +1,124 @@ +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 + +return Ease \ No newline at end of file diff --git a/prosperon/prosperon.ce b/prosperon/prosperon.ce index 55e59c36..aa95e8f0 100644 --- a/prosperon/prosperon.ce +++ b/prosperon/prosperon.ce @@ -6,11 +6,7 @@ var time = use('time') var tilemap = use('tilemap') // Frame timing variables -var frame_times = [] -var frame_time_index = 0 -var max_frame_samples = 60 -var frame_start_time = 0 -var average_frame_time = 0 +var framerate = 60 var game = args[0] @@ -34,40 +30,45 @@ $_.start(e => { var geometry = use('geometry') -function updateCameraMatrix(camera, winW, winH) { - // world→NDC - def sx = 1 / camera.size[0]; - def sy = 1 / camera.size[1]; - def ox = camera.pos[0] - camera.size[0] * camera.anchor[0]; - def oy = camera.pos[1] - camera.size[1] * camera.anchor[1]; +var camera = {} - // NDC→pixels - def vx = camera.viewport.x * winW; - def vy = camera.viewport.y * winH; - def vw = camera.viewport.width * winW; - def vh = camera.viewport.height * winH; +function updateCameraMatrix(cam) { + def win_w = logical.width + def win_h = logical.height + def view_w = (cam.size?.[0] ?? win_w) / cam.zoom + def view_h = (cam.size?.[1] ?? win_h) / cam.zoom - // final “mat” coefficients - // [ a 0 c ] - // [ 0 e f ] - // [ 0 0 1 ] - camera.a = sx * vw; - camera.c = vx - camera.a * ox; - camera.e = -sy * vh; - camera.f = vy + vh + sy * vh * oy; + def ox = cam.pos[0] - view_w * (cam.anchor?.[0] ?? 0) + def oy = cam.pos[1] - view_h * (cam.anchor?.[1] ?? 0) - // and store the inverses so we can go back cheaply - camera.ia = 1 / camera.a; - camera.ic = -camera.c * camera.ia; - camera.ie = 1 / camera.e; - camera.if = -camera.f * camera.ie; + def vx = (cam.viewport?.x ?? 0) * win_w + def vy = (cam.viewport?.y ?? 0) * win_h + def vw = (cam.viewport?.width ?? 1) * win_w + def vh = (cam.viewport?.height ?? 1) * win_h + + def sx = vw / view_w + def sy = vh / view_h // flip-Y later + + /* affine matrix that SDL wants (Y going down) */ + cam.a = sx + cam.c = vx - sx * ox + cam.e = -sy // <-- minus = flip Y + cam.f = vy + vh + sy * oy + + /* convenience inverses */ + cam.ia = 1 / cam.a + cam.ic = -cam.c / cam.a + cam.ie = 1 / cam.e + cam.if = -cam.f / cam.e + + camera = cam } //---- forward transform ---- -function worldToScreenPoint(pos, camera) { +function worldToScreenPoint([x,y], camera) { return { - x: camera.a * pos[0] + camera.c, - y: camera.e * pos[1] + camera.f + x: camera.a * x + camera.c, + y: camera.e * y + camera.f }; } @@ -80,40 +81,21 @@ function screenToWorldPoint(pos, camera) { } //---- rectangle (two corner) ---- -function worldToScreenRect(rect, camera) { +function worldToScreenRect({x,y,width,height}, camera) { // map bottom-left and top-right - def x1 = camera.a * rect.x + camera.c; - def y1 = camera.e * rect.y + camera.f; - def x2 = camera.a * (rect.x + rect.width) + camera.c; - def y2 = camera.e * (rect.y + rect.height) + camera.f; - - // pick mins and abs deltas - def x0 = x1 < x2 ? x1 : x2; - def y0 = y1 < y2 ? y1 : y2; + def x1 = camera.a * x + camera.c; + def y1 = camera.e * y + camera.f; + def x2 = camera.a * (x + width) + camera.c; + def y2 = camera.e * (y + height) + camera.f; + return { - x: x0, - y: y0, - width: x2 > x1 ? x2 - x1 : x1 - x2, - height: y2 > y1 ? y2 - y1 : y1 - y2 - }; + x:Math.min(x1,x2), + y:Math.min(y1,y2), + width:Math.abs(x2-x1), + height:Math.abs(y2-y1) + } } -var camera = { - size: [640,480],//{width:500,height:500}, // pixel size the camera "sees", like its resolution - pos: [250,250],//{x:0,y:0}, // where it is - fov:50, - near_z:0, - far_z:1000, - viewport: {x:0,y:0,width:1,height:1}, // viewport it appears on screen - ortho:true, - anchor:[0.5,0.5],//{x:0.5,y:0.5}, - rotation:[0,0,0,1], - surface: null -} - -var util = use('util') -var cammy = util.camera_globals(camera) - var graphics var gameactor @@ -122,12 +104,13 @@ var images = {} var renderer_commands = [] +var win_size = {width:500,height:500} +var logical = {width:500,height:500} + // Convert high-level draw commands to low-level renderer commands function translate_draw_commands(commands) { if (!graphics) return - updateCameraMatrix(camera,500,500) - renderer_commands.length = 0 commands.forEach(function(cmd) { @@ -140,6 +123,10 @@ function translate_draw_commands(commands) { } switch(cmd.cmd) { + case "camera": + updateCameraMatrix(cmd.camera, win_size.width, win_size.height) + break + case "draw_rect": cmd.rect = worldToScreenRect(cmd.rect, camera) // Handle rectangles with optional rounding and thickness @@ -208,7 +195,10 @@ function translate_draw_commands(commands) { case "draw_line": renderer_commands.push({ op: "line", - data: {points: cmd.points.map(p => worldToScreenPoint(p, camera))} + data: {points: cmd.points.map(p => { + var pt = worldToScreenPoint(p, camera) + return [pt.x, pt.y] + })} }) break @@ -238,13 +228,12 @@ function translate_draw_commands(commands) { } }) - log.console(json.encode(renderer_commands[renderer_commands.length-1])) break case "draw_text": if (!cmd.text) break if (!cmd.pos) break - var rect = worldToScreenRect({x:cmd.pos.x, y:cmd.pos.y, width:8, height:8}, camera, 500,500) + var rect = worldToScreenRect({x:cmd.pos.x, y:cmd.pos.y, width:8, height:8}, camera) var pos = {x: rect.x, y: rect.y} renderer_commands.push({ op: "debugText", @@ -293,38 +282,28 @@ function rpc_req(actor, msg) { } } -var game_rec = parseq.sequence([ - rpc_req(gameactor, {kind:'update', dt:1/60}), - rpc_req(gameactor, {kind:'draw'}) -]) - var pending_draw = null var pending_next = null var last_time = time.number() -var frames = [] -var frame_avg = 0 var input = use('input') var input_state = { - poll: 1/60 + poll: 1/framerate } // 1) input runs completely independently function poll_input() { send(video, {kind:'input', op:'get'}, evs => { for (var ev of evs) { + if (ev.type == 'window_pixel_size_changed') { + win_size.width = ev.width + win_size.height = ev.height + } + if (ev.type == 'quit') $_.stop() - if (ev.type.includes('mouse')) { - if (ev.pos) - ev.pos = screenToWorldPoint(ev.pos, camera, 500,500) - - if (ev.d_pos) - ev.d_pos.y *= -1 - } - if (ev.type.includes('key')) { if (ev.key) ev.key = input.keyname(ev.key) @@ -339,37 +318,25 @@ function poll_input() { // 2) helper to build & send a batch, then call done() function create_batch(draw_cmds, done) { def batch = [ - {op:'set', prop:'drawColor', value:[0.1,0.1,0.15,1]}, + {op:'set', prop:'drawColor', value:{r:0.1,g:0.1,b:0.15,a:1}}, {op:'clear'} ] if (draw_cmds && draw_cmds.length) batch.push(...translate_draw_commands(draw_cmds)) batch.push( - {op:'set', prop:'drawColor', value:[1,1,1,1]}, - {op:'debugText', data:{pos:{x:10,y:10}, text:`Fps: ${(1/frame_avg).toFixed(2)}`}}, + {op:'set', prop:'drawColor', value:{r:1,g:1,b:1,a:1}}, +// {op:'debugText', data:{pos:{x:10,y:10}, text:`Fps: ${(1/frame_avg).toFixed(2)}`}}, {op:'present'} ) - send(video, {kind:'renderer', op:'batch', data:batch}, () => { - def now = time.number() - def dt = now - last_time - last_time = now - - frames.push(dt) - if (frames.length > 60) frames.shift() - let sum = 0 - for (let f of frames) sum += f - frame_avg = sum / frames.length - - done(dt) - }) + send(video, {kind:'renderer', op:'batch', data:batch}, done) } // 3) kick off the very first update→draw function start_pipeline() { poll_input() - send(gameactor, {kind:'update', dt:1/60}, () => { + send(gameactor, {kind:'update', dt:0}, () => { send(gameactor, {kind:'draw'}, cmds => { pending_draw = cmds render_step() @@ -378,24 +345,26 @@ function start_pipeline() { } function render_step() { - // a) fire off the next update→draw immediately - def dt = time.number() - last_time - send(gameactor, {kind:'update', dt:1/60}, () => - send(gameactor, {kind:'draw'}, cmds => pending_next = cmds) - ) - - // c) render the current frame - create_batch(pending_draw, ttr => { // time to render - // only swap in when there's a new set of commands - if (pending_next) { - pending_draw = pending_next - pending_next = null - } - - // d) schedule the next render step - def render_dur = time.number() - last_time - def wait = Math.max(0, 1/60 - ttr) - $_.delay(render_step, 0) + // a) Calculate actual dt since last frame + def now = time.number() + def dt = now - last_time + last_time = now + + // b) Send update with actual dt, then wait for draw response + send(gameactor, {kind:'update', dt}, () => { + send(gameactor, {kind:'draw'}, cmds => { + // Only render after receiving draw commands + pending_draw = cmds + + // c) render the current frame + create_batch(pending_draw, _ => { // time to render + def frame_end = time.number() + def wait_time = Math.max(0, (frame_end - now) - 1/framerate) + + // e) Schedule next frame + $_.delay(render_step, wait_time) + }) + }) }) } @@ -409,6 +378,15 @@ $_.receiver(e => { prop:'logicalPresentation', value: {...e} }) + logical.width = e.width + logical.height = e.height + break + case 'framerate': + // Allow setting target framerate dynamically + if (e.fps && e.fps > 0) { + framerate = e.fps + input_state.poll = 1/framerate + } break } }) diff --git a/prosperon/sdl_video.ce b/prosperon/sdl_video.ce index b56c872e..1208c5a7 100644 --- a/prosperon/sdl_video.ce +++ b/prosperon/sdl_video.ce @@ -87,6 +87,8 @@ $_.receiver(function(msg) { var response = {}; +// log.console(json.encode(msg)) + try { switch (msg.kind) { case 'window': diff --git a/prosperon/sound.cm b/prosperon/sound.cm index ea199ec4..7db6bc9c 100644 --- a/prosperon/sound.cm +++ b/prosperon/sound.cm @@ -14,7 +14,7 @@ var pcms = {}; audio.pcm = function pcm(file) { file = res.find_sound(file); - if (!file) throw new Error(`Could not findfile ${file}`); + if (!file) return//throw new Error(`Could not findfile ${file}`); if (pcms[file]) return pcms[file]; var bytes = io.slurpbytes(file) var newpcm = soloud.load_wav_mem(io.slurpbytes(file)); @@ -88,9 +88,6 @@ var BYTES_PER_F = 4 var SAMPLES = FRAMES * CHANNELS var CHUNK_BYTES = FRAMES * CHANNELS * BYTES_PER_F -var mixview = new Float32Array(FRAMES*CHANNELS) -var mixbuf = mixview.buffer - function pump() { if (feeder.queued() < CHUNK_BYTES*3) { diff --git a/prosperon/tween.cm b/prosperon/tween.cm index f64770e7..dd12bb79 100644 --- a/prosperon/tween.cm +++ b/prosperon/tween.cm @@ -1,281 +1,79 @@ -var util = use('util') +var Ease = use('ease') +var time = use('time') -var Ease = { - linear(t) { - return t +var rate = 1/240 + +var TweenEngine = { + tweens: [], + add(tween) { + this.tweens.push(tween) }, - 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 + remove(tween) { + this.tweens = this.tweens.filter(t => t != tween) }, + update(dt) { + var now = time.number() + for (var tween of this.tweens.slice()) { + tween._update(now) + } + + $_.delay(_ => TweenEngine.update(), rate) + } } -function make_easing_fns(num) { - var obj = {} +function Tween(obj) { + this.obj = obj + this.startVals = {} + this.endVals = {} + this.duration = 0 + this.easing = Ease.linear + this.startTime = 0 + this.onCompleteCallback = function() {} +} - obj.in = function (t) { - return Math.pow(t, num) +Tween.prototype.to = function(props, duration) { + for (var key in props) { + this.startVals[key] = this.obj[key] + this.endVals[key] = props[key] + } + this.duration = duration + this.startTime = time.number() + + TweenEngine.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._update = function(now) { + var elapsed = now - this.startTime + var t = Math.min(elapsed / this.duration, 1) + var eased = this.easing(t) + + for (var key in this.endVals) { + var start = this.startVals[key] + var end = this.endVals[key] + this.obj[key] = start + (end - start) * eased } - obj.out = function (t) { - return 1 - Math.pow(1 - t, num) + if (t == 1) { + this.onCompleteCallback() + TweenEngine.remove(this) } - - 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 - }, +function tween(obj) { + return new Tween(obj) } -Ease.bounce = { - in(t) { - return 1 - this.out(t - 1) - }, - 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 - }, -} +$_.delay(_ => TweenEngine.update(), rate) -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) { - 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 - -var tween = function (from, to, time, fn, cb) { - var start = os.now() - - function cleanup() { - stop() - fn = null - stop = null - cb = null - update = null - } - - var update = function tween_update(dt) { - var elapsed = os.now() - start - fn(util.obj_lerp(from, to, elapsed / time)) - if (elapsed >= time) { - fn(to) - cb?.() - cleanup() - } - } - var stop = Register.update.register(update) - return cleanup -} - -var Tween = { - default: { - loop: "hold", - time: 1, - ease: Ease.linear, - whole: true, - cb: function () {}, - }, - start(obj, target, tvals, options) { - var defn = Object.create(this.default) - Object.assign(defn, options) - - if (defn.loop == "circle") tvals.push(tvals[0]) - else if (defn.loop == "yoyo") { - for (var i = tvals.length - 2; i >= 0; i--) tvals.push(tvals[i]) - } - - defn.accum = 0 - var slices = tvals.length - 1 - var slicelen = 1 / slices - - defn.fn = function (dt) { - defn.accum += dt - if (defn.accum >= defn.time && defn.loop == "hold") { - if (typeof target == "string") obj[target] = tvals[tvals.length - 1] - else target(tvals[tvals.length - 1]) - defn.pause() - defn.cb.call(obj) - return - } - defn.pct = (defn.accum % defn.time) / defn.time - if (defn.loop == "none" && defn.accum >= defn.time) defn.stop() - - var t = defn.whole ? defn.ease(defn.pct) : defn.pct - var nval = t / slicelen - var i = Math.trunc(nval) - nval -= i - if (!defn.whole) nval = defn.ease(nval) - - if (typeof target == "string") obj[target] = tvals[i].lerp(tvals[i + 1], nval) - else target(tvals[i].lerp(tvals[i + 1], nval)) - } - - var playing = false - - defn.play = function () { - if (playing) return - defn._end = Register.update.register(defn.fn.bind(defn)) - playing = true - } - defn.restart = function () { - defn.accum = 0 - if (typeof target == "string") obj[target] = tvals[0] - else target(tvals[0]) - } - defn.stop = function () { - if (!playing) return - defn.pause() - defn.restart() - } - defn.pause = function () { - defn._end() - if (!playing) return - playing = false - } - - return defn - }, -} - -Tween.make = Tween.start - -Ease[cell.DOC] = ` -This object provides multiple easing functions that remap a 0..1 input to produce -a smoothed or non-linear output. They can be used standalone or inside tweens. - -Available functions: -- linear(t) -- in(t), out(t), inout(t) -- quad.in, quad.out, quad.inout -- cubic.in, cubic.out, cubic.inout -- quart.in, quart.out, quart.inout -- quint.in, quint.out, quint.inout -- expo.in, expo.out, expo.inout -- bounce.in, bounce.out, bounce.inout -- sine.in, sine.out, sine.inout -- elastic.in, elastic.out, elastic.inout - -All easing functions expect t in [0..1] and return a remapped value in [0..1]. -` - -tween[cell.DOC] = ` -:param from: The starting object or value to interpolate from. -:param to: The ending object or value to interpolate to. -:param time: The total duration of the tween in milliseconds or some time unit. -:param fn: A callback function that receives the interpolated value at each update. -:param cb: (Optional) A callback invoked once the tween completes. -:return: A function that, when called, cleans up and stops the tween. - -Creates a simple tween that linearly interpolates from "from" to "to" over "time" -and calls "fn" with each interpolated value. Once finished, "fn" is called with "to", -then "cb" is invoked if provided, and the tween is cleaned up. -` - -Tween[cell.DOC] = ` -An object providing methods to create and control tweens with additional features -like looping, custom easing, multiple stages, etc. - -Properties: -- default: A template object with loop/time/ease/whole/cb properties. -Methods: -- start(obj, target, tvals, options): Create a tween over multiple target values. -- make: Alias of start. -` - -Tween.start[cell.DOC] = ` -:param obj: The object whose property is being tweened, or context for the callback. -:param target: A string property name in obj or a callback function receiving interpolated values. -:param tvals: An array of values to tween through (each must support .lerp()). -:param options: An optional object overriding defaults (loop type, time, ease, etc.). -:return: A tween definition object with .play(), .pause(), .stop(), .restart(), etc. - -Set up a multi-stage tween. You can specify looping modes (none, hold, restart, yoyo, circle), -time is the total duration, and "ease" can be any function from Ease. Once started, it updates -every frame until completion or stop/pause is called. -` - -Tween.make[cell.DOC] = ` -Alias of Tween.start. See Tween.start for usage details. -` - -return { Tween, Ease, tween } +return tween \ No newline at end of file diff --git a/scripts/engine.cm b/scripts/engine.cm index 9b51ec93..69020fc5 100644 --- a/scripts/engine.cm +++ b/scripts/engine.cm @@ -43,7 +43,7 @@ var console_mod = cell.hidden.console globalThis.log = {} log.console = function(msg) { - var caller = caller_data(2) + var caller = caller_data(1) console_mod.print(console_rec(caller.line, caller.file, msg)) } @@ -205,7 +205,7 @@ globalThis.use = function use(file, ...args) { } else { // Compile from source var script = io.slurp(path) - var mod_script = `(function setup_${mod_name}_module(arg){${script};})` + var mod_script = `(function setup_${mod_name}_module(arg, $_){${script};})` fn = js.compile(path, mod_script) // Save compiled version to .cell directory @@ -221,7 +221,7 @@ globalThis.use = function use(file, ...args) { context.__proto__ = embed_mod // Call the script - pass embedded module as 'this' if it exists - var ret = fn.call(context, args) + var ret = fn.call(context, args, $_) // If script doesn't return anything, check if we have embedded module if (!ret && embed_mod) { diff --git a/source/jsffi.c b/source/jsffi.c index 045bba83..ae114698 100644 --- a/source/jsffi.c +++ b/source/jsffi.c @@ -381,17 +381,37 @@ typedef HMM_Vec4 colorf; colorf js2color(JSContext *js,JSValue v) { if (JS_IsNull(v)) return (colorf){1,1,1,1}; - JSValue c[4]; - for (int i = 0; i < 4; i++) c[i] = JS_GetPropertyUint32(js,v,i); - float a = JS_IsNull(c[3]) ? 1.0 : js2number(js,c[3]); - colorf color = { - .r = js2number(js,c[0]), - .g = js2number(js,c[1]), - .b = js2number(js,c[2]), - .a = a, - }; - - for (int i = 0; i < 4; i++) JS_FreeValue(js,c[i]); + + colorf color = {1,1,1,1}; // Default to white + + if (JS_IsArray(js, v)) { + // Handle array format: [r, g, b, a] + JSValue c[4]; + for (int i = 0; i < 4; i++) c[i] = JS_GetPropertyUint32(js,v,i); + + color.r = js2number(js,c[0]); + color.g = js2number(js,c[1]); + color.b = js2number(js,c[2]); + color.a = JS_IsNull(c[3]) ? 1.0 : js2number(js,c[3]); + + for (int i = 0; i < 4; i++) JS_FreeValue(js,c[i]); + } else if (JS_IsObject(v)) { + // Handle object format: {r, g, b, a} + JSValue r_val = JS_GetPropertyStr(js, v, "r"); + JSValue g_val = JS_GetPropertyStr(js, v, "g"); + JSValue b_val = JS_GetPropertyStr(js, v, "b"); + JSValue a_val = JS_GetPropertyStr(js, v, "a"); + + color.r = JS_IsNull(r_val) ? 1.0 : js2number(js, r_val); + color.g = JS_IsNull(g_val) ? 1.0 : js2number(js, g_val); + color.b = JS_IsNull(b_val) ? 1.0 : js2number(js, b_val); + color.a = JS_IsNull(a_val) ? 1.0 : js2number(js, a_val); + + JS_FreeValue(js, r_val); + JS_FreeValue(js, g_val); + JS_FreeValue(js, b_val); + JS_FreeValue(js, a_val); + } return color; } diff --git a/source/qjs_sdl_input.c b/source/qjs_sdl_input.c index 8f7d22d1..18475109 100644 --- a/source/qjs_sdl_input.c +++ b/source/qjs_sdl_input.c @@ -255,7 +255,7 @@ static int event2wota_count_props(const SDL_Event *event) case SDL_EVENT_DISPLAY_DESKTOP_MODE_CHANGED: case SDL_EVENT_DISPLAY_CURRENT_MODE_CHANGED: case SDL_EVENT_DISPLAY_CONTENT_SCALE_CHANGED: - count += 3; // which, data1, data2 + count += 3; // which, orientation/data1, data2 break; case SDL_EVENT_MOUSE_MOTION: @@ -344,7 +344,7 @@ static int event2wota_count_props(const SDL_Event *event) case SDL_EVENT_WINDOW_LEAVE_FULLSCREEN: case SDL_EVENT_WINDOW_DESTROYED: case SDL_EVENT_WINDOW_HDR_STATE_CHANGED: - // which, data1, data2 => 3 extra + // which, x/width/display_index, y/height => 3 extra count += 3; break; @@ -427,6 +427,13 @@ static void event2wota_write(WotaBuffer *wb, const SDL_Event *e, int c) { wota_write_sym(wb, e->adevice.recording ? WOTA_TRUE : WOTA_FALSE); break; case SDL_EVENT_DISPLAY_ORIENTATION: + wota_write_text(wb, "which"); + wota_write_number(wb, (double)e->display.displayID); + wota_write_text(wb, "orientation"); + wota_write_number(wb, (double)e->display.data1); + wota_write_text(wb, "data2"); + wota_write_number(wb, (double)e->display.data2); + break; case SDL_EVENT_DISPLAY_ADDED: case SDL_EVENT_DISPLAY_REMOVED: case SDL_EVENT_DISPLAY_MOVED: @@ -552,10 +559,6 @@ static void event2wota_write(WotaBuffer *wb, const SDL_Event *e, int c) { case SDL_EVENT_WINDOW_SHOWN: case SDL_EVENT_WINDOW_HIDDEN: case SDL_EVENT_WINDOW_EXPOSED: - case SDL_EVENT_WINDOW_MOVED: - case SDL_EVENT_WINDOW_RESIZED: - case SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED: - case SDL_EVENT_WINDOW_METAL_VIEW_RESIZED: case SDL_EVENT_WINDOW_MINIMIZED: case SDL_EVENT_WINDOW_MAXIMIZED: case SDL_EVENT_WINDOW_RESTORED: @@ -566,14 +569,12 @@ static void event2wota_write(WotaBuffer *wb, const SDL_Event *e, int c) { case SDL_EVENT_WINDOW_CLOSE_REQUESTED: case SDL_EVENT_WINDOW_HIT_TEST: case SDL_EVENT_WINDOW_ICCPROF_CHANGED: - case SDL_EVENT_WINDOW_DISPLAY_CHANGED: - case SDL_EVENT_WINDOW_DISPLAY_SCALE_CHANGED: - case SDL_EVENT_WINDOW_SAFE_AREA_CHANGED: case SDL_EVENT_WINDOW_OCCLUDED: case SDL_EVENT_WINDOW_ENTER_FULLSCREEN: case SDL_EVENT_WINDOW_LEAVE_FULLSCREEN: case SDL_EVENT_WINDOW_DESTROYED: case SDL_EVENT_WINDOW_HDR_STATE_CHANGED: + case SDL_EVENT_WINDOW_SAFE_AREA_CHANGED: wota_write_text(wb, "which"); wota_write_number(wb, (double)e->window.windowID); wota_write_text(wb, "data1"); @@ -581,6 +582,33 @@ static void event2wota_write(WotaBuffer *wb, const SDL_Event *e, int c) { wota_write_text(wb, "data2"); wota_write_number(wb, (double)e->window.data2); break; + case SDL_EVENT_WINDOW_MOVED: + wota_write_text(wb, "which"); + wota_write_number(wb, (double)e->window.windowID); + wota_write_text(wb, "x"); + wota_write_number(wb, (double)e->window.data1); + wota_write_text(wb, "y"); + wota_write_number(wb, (double)e->window.data2); + break; + case SDL_EVENT_WINDOW_RESIZED: + case SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED: + case SDL_EVENT_WINDOW_METAL_VIEW_RESIZED: + wota_write_text(wb, "which"); + wota_write_number(wb, (double)e->window.windowID); + wota_write_text(wb, "width"); + wota_write_number(wb, (double)e->window.data1); + wota_write_text(wb, "height"); + wota_write_number(wb, (double)e->window.data2); + break; + case SDL_EVENT_WINDOW_DISPLAY_CHANGED: + case SDL_EVENT_WINDOW_DISPLAY_SCALE_CHANGED: + wota_write_text(wb, "which"); + wota_write_number(wb, (double)e->window.windowID); + wota_write_text(wb, "display_index"); + wota_write_number(wb, (double)e->window.data1); + wota_write_text(wb, "data2"); + wota_write_number(wb, (double)e->window.data2); + break; case SDL_EVENT_JOYSTICK_ADDED: case SDL_EVENT_JOYSTICK_REMOVED: case SDL_EVENT_JOYSTICK_UPDATE_COMPLETE: diff --git a/source/qjs_sdl_video.c b/source/qjs_sdl_video.c index 687fd652..b36be1fa 100644 --- a/source/qjs_sdl_video.c +++ b/source/qjs_sdl_video.c @@ -820,7 +820,7 @@ JSC_CCALL(renderer_point, ) JSC_CCALL(renderer_texture, - SDL_Renderer *r = js2SDL_Renderer(js, self); + SDL_Renderer *ren = js2SDL_Renderer(js, self); SDL_Texture *tex = js2SDL_Texture(js,argv[0]); rect src = js2rect(js,argv[1]); rect dst = js2rect(js,argv[2]); @@ -830,7 +830,13 @@ JSC_CCALL(renderer_texture, HMM_Vec2 anchor = js2vec2(js, argv[4]); anchor.y = dst.h - anchor.y*dst.h; anchor.x *= dst.w; - SDL_RenderTextureRotated(r, tex, &src, &dst, angle, &anchor, SDL_FLIP_NONE); + float r,g,b,a; + SDL_GetRenderDrawColorFloat(ren, &r,&g,&b,&a); + SDL_SetTextureColorModFloat(tex, r,g,b); + SDL_SetTextureAlphaModFloat(tex, a); + SDL_RenderTextureRotated(ren, tex, &src, &dst, angle, &anchor, SDL_FLIP_NONE); + SDL_SetTextureColorModFloat(tex,1,1,1); + SDL_SetTextureAlphaModFloat(tex,a); ) JSC_CCALL(renderer_rects,