180 lines
9.5 KiB
Text
180 lines
9.5 KiB
Text
# emit_anim.ludic — 2D animation: the Anim.* (spritesheet frame animation) and
|
|
# Tween.* (value interpolation over a timeline) namespaces. Both are pure,
|
|
# deterministic Q16.16 / integer math driven off the game's fixed frame clock —
|
|
# the game stores a timer in a component and calls these each frame, exactly the
|
|
# way Collision.* / Grid.* are used. Same inputs -> same frame and same eased
|
|
# value on every run, so replays and lockstep netcode reproduce motion exactly.
|
|
#
|
|
# Conventions: a `timer` is elapsed seconds as a fixed (Q16.16); `fps` and frame
|
|
# counts are plain ints; interpolation amounts `t` are a fixed in 0.0..1.0.
|
|
|
|
# ------------------------------------------------------------------ Anim.* ----
|
|
# spritesheet frame animation: turn an elapsed timer into the frame index to
|
|
# draw. floor(timer * fps) is the number of whole frames elapsed; the flavour
|
|
# (loop / once / ping-pong) decides how that maps back into 0..count-1.
|
|
|
|
function is_anim_ns(meth: pointer) -> bool {
|
|
if (meth == "frame") or (meth == "once") or (meth == "pingpong") { return true }
|
|
if (meth == "finished") or (meth == "duration") { return true }
|
|
if (meth == "cell_x") or (meth == "cell_y") { return true }
|
|
return false
|
|
}
|
|
|
|
# floor(timer * fps) -> i32 code of whole frames elapsed (i64 intermediate so a
|
|
# long-running timer can't overflow the multiply).
|
|
function anim_elapsed(timer: pointer, fps: pointer) -> pointer {
|
|
let t64 = emit_bind(`sext i32 {timer} to i64`)
|
|
let f64 = emit_bind(`sext i32 {fps} to i64`)
|
|
let m = emit_bind(`mul i64 {t64}, {f64}`) # Q16.16 frames
|
|
let sh = emit_bind(`ashr i64 {m}, 16`)
|
|
return emit_bind(`trunc i64 {sh} to i32`)
|
|
}
|
|
|
|
# max(1, v) — guard a divisor / modulus against a zero or negative count.
|
|
function anim_atleast1(v: pointer) -> pointer {
|
|
let c = emit_bind(`icmp slt i32 {v}, 1`)
|
|
return emit_bind(`select i1 {c}, i32 1, i32 {v}`)
|
|
}
|
|
|
|
function emit_anim_ns(meth: pointer, e: Node) -> Val {
|
|
if (meth == "frame") { # looping frame: elapsed mod count
|
|
let timer = emit_expr(e.kids[0]); let fps = emit_expr(e.kids[1]); let count = emit_expr(e.kids[2])
|
|
let el = anim_elapsed(timer.code, fps.code)
|
|
let cnt = anim_atleast1(count.code)
|
|
return val(emit_bind(`srem i32 {el}, {cnt}`), "int")
|
|
}
|
|
if (meth == "once") { # one-shot: min(elapsed, count-1)
|
|
let timer = emit_expr(e.kids[0]); let fps = emit_expr(e.kids[1]); let count = emit_expr(e.kids[2])
|
|
let el = anim_elapsed(timer.code, fps.code)
|
|
let last = emit_bind(`sub i32 {count.code}, 1`)
|
|
let c = emit_bind(`icmp slt i32 {el}, {last}`)
|
|
return val(emit_bind(`select i1 {c}, i32 {el}, i32 {last}`), "int")
|
|
}
|
|
if (meth == "pingpong") { # bounce 0..count-1..0
|
|
let timer = emit_expr(e.kids[0]); let fps = emit_expr(e.kids[1]); let count = emit_expr(e.kids[2])
|
|
let el = anim_elapsed(timer.code, fps.code)
|
|
let two = emit_bind(`mul i32 {count.code}, 2`)
|
|
let p2 = emit_bind(`sub i32 {two}, 2`) # 2*count - 2
|
|
let period = anim_atleast1(p2)
|
|
let m = emit_bind(`srem i32 {el}, {period}`)
|
|
let back = emit_bind(`sub i32 {period}, {m}`)
|
|
let c = emit_bind(`icmp slt i32 {m}, {count.code}`)
|
|
return val(emit_bind(`select i1 {c}, i32 {m}, i32 {back}`), "int")
|
|
}
|
|
if (meth == "finished") { # has a one-shot run past its last frame?
|
|
let timer = emit_expr(e.kids[0]); let fps = emit_expr(e.kids[1]); let count = emit_expr(e.kids[2])
|
|
let el = anim_elapsed(timer.code, fps.code)
|
|
let c = emit_bind(`icmp sge i32 {el}, {count.code}`)
|
|
return val(emit_bind(`zext i1 {c} to i32`), "bool")
|
|
}
|
|
if (meth == "duration") { # seconds for one cycle: count / fps -> fixed
|
|
let fps = emit_expr(e.kids[0]); let count = emit_expr(e.kids[1])
|
|
let num = emit_bind(`shl i32 {count.code}, 16`) # count as fixed
|
|
let den = anim_atleast1(fps.code)
|
|
return val(emit_bind(`sdiv i32 {num}, {den}`), "fixed")
|
|
}
|
|
if (meth == "cell_x") { # source x of a frame: (frame mod cols) * cell_w
|
|
let frame = emit_expr(e.kids[0]); let cols = emit_expr(e.kids[1]); let cw = emit_expr(e.kids[2])
|
|
let c1 = anim_atleast1(cols.code)
|
|
let col = emit_bind(`srem i32 {frame.code}, {c1}`)
|
|
return val(emit_bind(`mul i32 {col}, {cw.code}`), "int")
|
|
}
|
|
# cell_y — source y of a frame: (frame / cols) * cell_h
|
|
let frame = emit_expr(e.kids[0]); let cols = emit_expr(e.kids[1]); let ch = emit_expr(e.kids[2])
|
|
let c1 = anim_atleast1(cols.code)
|
|
let row = emit_bind(`sdiv i32 {frame.code}, {c1}`)
|
|
return val(emit_bind(`mul i32 {row}, {ch.code}`), "int")
|
|
}
|
|
|
|
# ----------------------------------------------------------------- Tween.* ----
|
|
# value interpolation over a timeline. The timeline helpers turn (timer,
|
|
# duration) into a normalized amount with a chosen boundary behaviour; ease()
|
|
# shapes that amount through one of the engine's easing curves (shared with
|
|
# Ease.*); the typed interpolators blend two endpoints by an amount.
|
|
|
|
function is_tween_ns(meth: pointer) -> bool {
|
|
if (meth == "progress") or (meth == "loop") or (meth == "yoyo") or (meth == "done") { return true }
|
|
if (meth == "ease") or (meth == "number") or (meth == "round") { return true }
|
|
if (meth == "point") or (meth == "tint") { return true }
|
|
return false
|
|
}
|
|
|
|
# a positive divisor for the timeline: duration if > 0, else 1.0 (65536).
|
|
function tween_den(dur: pointer) -> pointer {
|
|
let dpos = emit_bind(`icmp sgt i32 {dur}, 0`)
|
|
return emit_bind(`select i1 {dpos}, i32 {dur}, i32 65536`)
|
|
}
|
|
|
|
function emit_tween_ns(meth: pointer, e: Node) -> Val {
|
|
if (meth == "progress") { # clamp(timer / duration, 0, 1) -> fixed
|
|
let timer = emit_expr(e.kids[0]); let dur = emit_expr(e.kids[1])
|
|
let dpos = emit_bind(`icmp sgt i32 {dur.code}, 0`)
|
|
let den = emit_bind(`select i1 {dpos}, i32 {dur.code}, i32 65536`)
|
|
let r = fx_div_code(timer.code, den)
|
|
let neg = emit_bind(`icmp slt i32 {r}, 0`)
|
|
let lo = emit_bind(`select i1 {neg}, i32 0, i32 {r}`)
|
|
let over = emit_bind(`icmp sgt i32 {lo}, 65536`)
|
|
let r1 = emit_bind(`select i1 {over}, i32 65536, i32 {lo}`)
|
|
return val(emit_bind(`select i1 {dpos}, i32 {r1}, i32 65536`), "fixed") # dur<=0 -> done
|
|
}
|
|
if (meth == "loop") { # frac(timer / duration) in [0,1) -> fixed
|
|
let timer = emit_expr(e.kids[0]); let dur = emit_expr(e.kids[1])
|
|
let den = tween_den(dur.code)
|
|
let r = fx_div_code(timer.code, den)
|
|
return val(emit_bind(`and i32 {r}, 65535`), "fixed") # nonneg fractional part
|
|
}
|
|
if (meth == "yoyo") { # triangle 0..1..0 over the duration -> fixed
|
|
let timer = emit_expr(e.kids[0]); let dur = emit_expr(e.kids[1])
|
|
let den = tween_den(dur.code)
|
|
let r = fx_div_code(timer.code, den)
|
|
let u = emit_bind(`srem i32 {r}, 131072`) # mod 2.0
|
|
let back = emit_bind(`sub i32 131072, {u}`)
|
|
let c = emit_bind(`icmp sle i32 {u}, 65536`)
|
|
return val(emit_bind(`select i1 {c}, i32 {u}, i32 {back}`), "fixed")
|
|
}
|
|
if (meth == "done") {
|
|
# 1-arg Tween.done(handle) -> the fluent stateful handle's completion (#48),
|
|
# a call into tween.ludic; the 2-arg Tween.done(timer, dur) is the pure form.
|
|
if (len(e.kids) == 1) {
|
|
let h = emit_expr(e.kids[0])
|
|
return val(emit_bind(`call i32 {fn_sym("tween_done")}(i32 {h.code})`), "bool")
|
|
}
|
|
let timer = emit_expr(e.kids[0]); let dur = emit_expr(e.kids[1]) # timer >= duration -> bool
|
|
let c = emit_bind(`icmp sge i32 {timer.code}, {dur.code}`)
|
|
return val(emit_bind(`zext i1 {c} to i32`), "bool")
|
|
}
|
|
if (meth == "ease") { # ease(t, mode) -> fixed; mode is a literal 0..6
|
|
let t = emit_expr(e.kids[0])
|
|
let m = e.kids[1]
|
|
if (m.kind != E_INT) { perr("Tween.ease: the easing mode must be a literal int 0..6") }
|
|
return val(ease_eval(m.ival, t.code), "fixed")
|
|
}
|
|
if (meth == "number") { # lerp two fixeds by t -> fixed
|
|
let a = emit_expr(e.kids[0]); let b = emit_expr(e.kids[1]); let t = emit_expr(e.kids[2])
|
|
return val(fx_lerp_code(a.code, b.code, t.code), "fixed")
|
|
}
|
|
if (meth == "round") { # lerp two ints by t, rounded -> int
|
|
let a = emit_expr(e.kids[0]); let b = emit_expr(e.kids[1]); let t = emit_expr(e.kids[2])
|
|
let d = emit_bind(`sub i32 {b.code}, {a.code}`)
|
|
let d64 = emit_bind(`sext i32 {d} to i64`)
|
|
let t64 = emit_bind(`sext i32 {t.code} to i64`)
|
|
let p = emit_bind(`mul i64 {d64}, {t64}`) # Q16.16
|
|
let p2 = emit_bind(`add i64 {p}, 32768`) # + 0.5
|
|
let sh = emit_bind(`ashr i64 {p2}, 16`)
|
|
let dt = emit_bind(`trunc i64 {sh} to i32`)
|
|
return val(emit_bind(`add i32 {a.code}, {dt}`), "int")
|
|
}
|
|
if (meth == "point") { # lerp two Vectors by t -> Vector
|
|
let a = emit_expr(e.kids[0]); let b = emit_expr(e.kids[1]); let t = emit_expr(e.kids[2])
|
|
let ax = vec_x(a.code); let ay = vec_y(a.code); let bx = vec_x(b.code); let by = vec_y(b.code)
|
|
let lx = fx_lerp_code(ax, bx, t.code)
|
|
let ly = fx_lerp_code(ay, by, t.code)
|
|
return val(vec_pack(lx, ly), "Vector")
|
|
}
|
|
# tint — blend two colors (0x00RRGGBB) by t, per channel -> int
|
|
let a = emit_expr(e.kids[0]); let b = emit_expr(e.kids[1]); let t = emit_expr(e.kids[2])
|
|
let r = color_lerp_ch(color_ch(a.code, "16"), color_ch(b.code, "16"), t.code)
|
|
let g = color_lerp_ch(color_ch(a.code, "8"), color_ch(b.code, "8"), t.code)
|
|
let bl = color_lerp_ch(color_ch(a.code, "0"), color_ch(b.code, "0"), t.code)
|
|
return val(color_pack(r, g, bl), "int")
|
|
}
|