port: settle at the hold, not at the last timed keyframe
P2 shipped the wrong rule and the title is the counter-example. I had reasoned that the exit is the final untimed keyframe, so anything timed was still the entry. It is not: `pteff02` on the title holds at t=46 with the 25 % dim quad at alpha 0x40 and then ramps to 0x00 by t=236. The exit can be a long run of TIMED keyframes, and running to the end drops the dim and leaves the whole screen ~13/255 too bright -- exactly the luminance excess P2 recorded (oracle 64.1, rest 62.8, timeline 80.0) and filed as an unexplained "glow slab". A group is pre-roll -> ramp in -> hold -> ramp out -> post-roll, and a screen that has arrived sits on the hold. `settle_units()` is now `rest.t`, the decoders' own identification of that hold. The check is that a disagreement vanishes: on ALL TWELVE screens the settled timeline is now byte-identical to the `--pose=rest` render, where before this four differed by up to 247/255. The timeline's endpoint should BE `rest` -- the animation is what it adds, not a different destination -- so this is the property to want, and it holds with no special case. Credit where due: this came out of the RE agent measuring the title's dim quad against a plate-free capture, in pursuit of a different question.
This commit is contained in:
@@ -117,14 +117,20 @@ static func _vec(a: Array) -> Vector2:
|
|||||||
|
|
||||||
## The pose of one element at `time_units`.
|
## The pose of one element at `time_units`.
|
||||||
##
|
##
|
||||||
## The timed keyframes are the whole timeline. Before the first, the element
|
## A group is `pre-roll -> ramp in -> HOLD -> ramp out -> post-roll`, and a
|
||||||
## holds its first pose (the pre-roll a staggered menu needs -- the five buttons
|
## screen that has arrived sits on the **hold**. So the timeline plays in and
|
||||||
## start at t=28,30,32,34,36). After the last TIMED keyframe it holds that pose.
|
## stops at `rest`, which is the decoders' identification of that hold and
|
||||||
|
## carries its own `t`.
|
||||||
##
|
##
|
||||||
## It never plays into the final, untimed keyframe. That frame is the screen's
|
## It is emphatically NOT "play to the last timed keyframe". The exit is not
|
||||||
## EXIT pose, and the disc gives no time slot for the ramp into it, so playing
|
## only the final untimed frame -- it can be a long run of TIMED ones. The
|
||||||
## it would mean inventing a duration. The exit is the transition, and it is
|
## title's `pteff02` holds at `t=46` with the 25 % dim quad at alpha 0x40 and
|
||||||
## P3's, with its own measured evidence. See `authored/timing.json`.
|
## then ramps to 0x00 by `t=236`; running to the end drops the dim and makes the
|
||||||
|
## whole screen ~13/255 too bright. That was measured against a plate-free
|
||||||
|
## capture of the running title, and it is what corrected this rule.
|
||||||
|
##
|
||||||
|
## Before the first keyframe the element holds its first pose -- the pre-roll a
|
||||||
|
## staggered menu needs, with the five buttons starting at t=28,30,32,34,36.
|
||||||
func pose_at(element: Dictionary, t: float) -> Dictionary:
|
func pose_at(element: Dictionary, t: float) -> Dictionary:
|
||||||
var frames: Array = element.get("keyframes", [])
|
var frames: Array = element.get("keyframes", [])
|
||||||
var timed: Array = []
|
var timed: Array = []
|
||||||
@@ -134,6 +140,10 @@ func pose_at(element: Dictionary, t: float) -> Dictionary:
|
|||||||
if timed.is_empty():
|
if timed.is_empty():
|
||||||
# No timed frame at all: the group is a single static pose.
|
# No timed frame at all: the group is a single static pose.
|
||||||
return frames[0] if not frames.is_empty() else element.get("rest", {})
|
return frames[0] if not frames.is_empty() else element.get("rest", {})
|
||||||
|
# Stop at the hold. Past it the group is ramping out, which is the screen
|
||||||
|
# transition and belongs to whatever is driving the transition -- not to a
|
||||||
|
# screen that has arrived and is sitting there.
|
||||||
|
t = minf(t, settle_units(element))
|
||||||
if t <= float(timed[0]["t"]):
|
if t <= float(timed[0]["t"]):
|
||||||
return timed[0]
|
return timed[0]
|
||||||
for i in range(timed.size() - 1):
|
for i in range(timed.size() - 1):
|
||||||
@@ -173,13 +183,26 @@ static func _hex_lerp(a: String, b: String, f: float) -> String:
|
|||||||
return "0x%08x" % out
|
return "0x%08x" % out
|
||||||
|
|
||||||
|
|
||||||
## The last moment anything on this screen is still moving, in keyframe units.
|
## Where one element stops, in keyframe units: its hold.
|
||||||
|
##
|
||||||
|
## `rest.t` when the export gives one. An element whose `rest` carries no time is
|
||||||
|
## a single static pose, and there the last timed keyframe is the same answer.
|
||||||
|
static func settle_units(element: Dictionary) -> float:
|
||||||
|
var rest: Dictionary = element.get("rest", {})
|
||||||
|
if rest.has("t"):
|
||||||
|
return float(rest["t"])
|
||||||
|
var last := 0.0
|
||||||
|
for k: Dictionary in element.get("keyframes", []):
|
||||||
|
if k.has("t"):
|
||||||
|
last = maxf(last, float(k["t"]))
|
||||||
|
return last
|
||||||
|
|
||||||
|
|
||||||
|
## The moment the whole screen has arrived: the last element to reach its hold.
|
||||||
func settle_time() -> float:
|
func settle_time() -> float:
|
||||||
var last := 0.0
|
var last := 0.0
|
||||||
for element: Dictionary in screen.get("elements", []):
|
for element: Dictionary in screen.get("elements", []):
|
||||||
for k: Dictionary in element.get("keyframes", []):
|
last = maxf(last, settle_units(element))
|
||||||
if k.has("t"):
|
|
||||||
last = maxf(last, float(k["t"]))
|
|
||||||
return last
|
return last
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user