Skip to content

Value at time v1 - #803

Open
pgilfernandez wants to merge 4 commits into
friction2d:mainfrom
pgilfernandez:value-at-time-v1
Open

Value at time v1#803
pgilfernandez wants to merge 4 commits into
friction2d:mainfrom
pgilfernandez:value-at-time-v1

Conversation

@pgilfernandez

@pgilfernandez pgilfernandez commented Aug 4, 2026

Copy link
Copy Markdown

Hi!

With the help of AI, this PR adds After Effects-style temporal sampling to Friction expressions through valueAtTime(), it's been requested/discussed here and here.

Expressions can now sample:

  • The base, pre-expression value of the property containing the expression.
  • The effective value of any bound property, including that property's own expression.
  • Fractional frames, using the property's normal interpolation.
  • Times before the first keyframe or after the last keyframe, using the normal timeline value-holding behavior.

Usage

Sample the current property

// Bindings
time = $time;

// Calculate
return valueAtTime(time - 0.5);

This samples the current property's keyframed, pre-expression value half a second earlier. Using the base value prevents self-recursion.

Sample another property

// Bindings
source = RectangleBox.transform.translation.x;
time = $time;

// Calculate
return valueAtTime("source", time - 0.5);

The first argument is the binding name, passed as a string. The sampled result is the effective value of the bound property, including its own expression.

New $time binding

This PR also introduces:

time = $time;

$time is the absolute scene time in seconds, calculated from the absolute scene frame and FPS.

Although expressions could previously calculate time using:

frame = $frame;
fps = $scene.fps;
time = frame / fps;

that is not always equivalent. $frame is relative to the property or layer and can be affected by frame shifts, while valueAtTime() uses absolute scene time.

For example, at 24 FPS, with a layer shifted by 48 frames and the scene at frame 120:

$frame / $scene.fps = (120 - 48) / 24 = 3 seconds
$time = 120 / 24 = 5 seconds

Using $time therefore provides an unambiguous scene clock and matches the temporal model expected by valueAtTime().

API

valueAtTime(time)
valueAtTime("bindingName", time)

The time argument:

  • Is expressed in seconds.
  • Uses the absolute scene timeline.
  • Accepts fractional and negative values within Friction's internal frame range.
  • Must be a finite JavaScript number.

Invalid binding names, argument types, FPS values, or internal frame ranges produce descriptive JavaScript errors.

Frame-shift handling

Sampling converts the requested time into an absolute scene frame and then converts that frame into the relative frame of the sampled property.

This ensures that expressions work correctly when the source and target properties belong to layers with different timeline shifts.

The same absolute-frame conversion was also applied to normal property binding evaluation at arbitrary frames.

Dependency and cycle handling

Existing expression dependency analysis also applies to temporal sampling.

Valid dependency:

A → B

Rejected circular dependency:

A → B
B → A

Cycles are rejected even when the expressions use different time offsets, because evaluating effective values would still create recursive expression dependencies.

The single-argument form is safe because it samples the current property's pre-expression value.

Expression invalidation

Expressions using temporal sampling are treated as time-dependent and non-static.

When a sampled binding changes, Friction conservatively invalidates the full expression range. This guarantees correctness for arbitrary time offsets. More precise dependency-range analysis could be added later as a performance optimization.

Editor integration

The expression editor now includes:

  • $time syntax highlighting.
  • $time autocomplete support.
  • valueAtTime(time) autocomplete.
  • valueAtTime("bindingName", time) autocomplete.
  • Validation using the expression property's current scene frame.

Using the current frame during validation fixes an issue where the editor's internal uninitialized-frame sentinel could incorrectly produce an out-of-range error.

Preset

A new Value at Time (Delay) expression preset is included.

It works immediately with the current property's base value:

// Bindings
source = $value;
time = $time;
// Calculate
var delaySeconds = 2;
var delayedTime = time - delaySeconds;

return valueAtTime("source", delayedTime);

The preset also explains how to replace $value with another mapped property.

Validation

It builds successfully on macOS, I guess it should work and any other OS.
I hope you like it 😊

Test builds

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant