fixes for gameplay

This commit is contained in:
2025-07-05 10:24:09 -05:00
parent 1bc34bb99c
commit d52d50fe61
12 changed files with 423 additions and 426 deletions

View File

@@ -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_<variant>` - 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

View File

@@ -1,4 +1,5 @@
var input = use('input')
return {}
var downkeys = {};

View File

@@ -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

124
prosperon/ease.cm Normal file
View File

@@ -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

View File

@@ -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;
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;
// pick mins and abs deltas
def x0 = x1 < x2 ? x1 : x2;
def y0 = y1 < y2 ? y1 : y2;
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)
)
// 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, 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
}
create_batch(pending_draw, _ => { // time to render
def frame_end = time.number()
def wait_time = Math.max(0, (frame_end - now) - 1/framerate)
// 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)
// 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
}
})

View File

@@ -87,6 +87,8 @@ $_.receiver(function(msg) {
var response = {};
// log.console(json.encode(msg))
try {
switch (msg.kind) {
case 'window':

View File

@@ -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) {

View File

@@ -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)
}
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(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
},
}
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()
$_.delay(_ => TweenEngine.update(), rate)
}
}
var stop = Register.update.register(update)
return cleanup
function Tween(obj) {
this.obj = obj
this.startVals = {}
this.endVals = {}
this.duration = 0
this.easing = Ease.linear
this.startTime = 0
this.onCompleteCallback = function() {}
}
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)
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()
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])
TweenEngine.add(this)
return this
}
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))
Tween.prototype.ease = function(easingFn) {
this.easing = easingFn
return this
}
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
Tween.prototype.onComplete = function(callback) {
this.onCompleteCallback = callback
return this
}
return defn
},
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
}
Tween.make = Tween.start
if (t == 1) {
this.onCompleteCallback()
TweenEngine.remove(this)
}
}
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.
function tween(obj) {
return new Tween(obj)
}
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
$_.delay(_ => TweenEngine.update(), rate)
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

View File

@@ -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) {

View File

@@ -381,17 +381,37 @@ typedef HMM_Vec4 colorf;
colorf js2color(JSContext *js,JSValue v) {
if (JS_IsNull(v)) return (colorf){1,1,1,1};
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);
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,
};
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;
}

View File

@@ -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:

View File

@@ -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,