Skip to content

Implemented eased distortions with shapeable ramps for imperceptible rate changes#14

Merged
boatmeme merged 3 commits into
mainfrom
easing
Jun 28, 2026
Merged

Implemented eased distortions with shapeable ramps for imperceptible rate changes#14
boatmeme merged 3 commits into
mainfrom
easing

Conversation

@boatmeme

@boatmeme boatmeme commented Jun 28, 2026

Copy link
Copy Markdown
Owner

Closes #9.

The ConstantTime* distortions are piecewise-linear, so the rate snaps at every window boundary — the visible tell the "ease into the slowing" use case wants to hide. This adds a distortion family whose rate varies smoothly across the window.

Fake-clock speed across a window: a constant distortion snaps from 1× to ½× at both seams, while the eased bump dips smoothly to a momentary pause at the midpoint and the eased plateau holds a gentle, sustained rate

What's new

  • EasedTimeDistortion — base class extending RelativeTimeDistortion. The rate follows a trapezoid with cosine-eased corners: a rising half-cosine ramp, a flat plateau, then a falling ramp. distortTime returns the difference of a normalized cumulative cumulativeFraction(u) at the slice endpoints.
  • EasedTimeDilation / EasedTimeCompression — semantic subclasses mirroring the Constant* pair; same three arguments, drop-in swap.

Shaping the easing

Optional fourth argument { ramp, rampIn, rampOut }:

  • ramp sets both ease regions; rampIn/rampOut override per side and win over the shorthand, CSS-padding style. A side of 0 is a hard, constant-rate seam.
  • Each accepts a Duration of real time (the natural unit for imperceptibility, which is an absolute-time effect) or a bare number as a fraction of the window (handy when driven by a normalized control like a slider).
  • Default ramp = 0.5 → the symmetric raised-cosine bump, where the ramps meet at the midpoint and peak deviation is twice the average. Narrowing the ramps opens a plateau, decoupling peak from average so the slowdown can be sustained and gentle, or asymmetric (long onset, quick roll-off). Overlapping ramps are clamped proportionally, not rejected.

The endpoints are limiting cases of ramp: 0.5 is the bump, 0 collapses to the hard-seam ConstantTime* behavior.

Why not Gaussian

A Gaussian bump is the same single-hump character as the cosine but costs an erf approximation, and a truncated Gaussian doesn't reach zero at the seams without a pedestal correction. It was dropped: the plateau fills the genuinely different "sustained gentle" need, and the cosine ramp is already C¹ at the boundaries.

Invariants (both called out in the issue)

  • Polling-frequency independencedistortTime is the slice integral, never a point sample, so summing arbitrary uneven slices equals the whole-window value. Tested at the distortion level and at the Clock level (coarse single poll vs per-minute), including a non-default plateau ramp.
  • Whole-window contractcumulativeFraction runs 0 → 1 for any ramp, so the window still nets exactly relative - reference; the README round-trip returns to perfect sync by 10am, same as the constant version. A dedicated test confirms ~1:1 speed at every seam while the distortion is deepest mid-window.

The eased path keeps the constant distortion's exact endpoints — both fall 3h behind by 7am and return to zero by 10am — but meets every seam with a flat tangent instead of a corner:

Fake-clock lag across the example night: both the constant and eased clocks fall exactly 3h behind by 7am and return to zero by 10am, but the constant path has sharp corners while the eased path has flat tangents at every seam

Extensibility

Custom curves subclass EasedTimeDistortion and override cumulativeFraction — the base integrates it per slice. README shows a smoothstep example.

Tests / checks

All 87 tests pass under the CI config; 100% coverage on the new file; eslint and tsc --noEmit clean.

🤖 Generated with Claude Code

boatmeme and others added 3 commits June 28, 2026 00:53
…anges

The ConstantTime* distortions apply a flat rate across the whole window, so
the clock's speed snaps open at window start, closed at window end, and at
every hand-off in between -- the exact tell the bedside-clock use case is
trying to hide.

Add an EasedTimeDistortion family whose rate varies smoothly across the
window: a raised-cosine bump that eases up from normal speed, peaks at the
midpoint, and eases back out, with no kink at the seams. EasedTimeDilation
and EasedTimeCompression are drop-in replacements for the constant pair,
taking the same three arguments.

distortTime returns the definite integral of the rate over
[offset, offset + length] -- the difference of a normalized cumulative at the
slice endpoints -- not a point sample, so the offset Clock accumulates stays
independent of polling frequency. cumulativeFraction(0)=0 and
cumulativeFraction(1)=1 keep the whole-window contract intact: over
referenceDuration of real time the fake clock still advances exactly
relativeDuration, so composed windows net out identically to the constant
case. Custom curves (Gaussian, smoothstep) subclass EasedTimeDistortion and
override cumulativeFraction.

Closes #9

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…te changes

The ConstantTime* distortions apply a flat rate across the whole window, so the
clock's speed snaps open at window start, closed at window end, and at every
hand-off in between -- the exact tell the bedside-clock use case is trying to
hide.

Add an EasedTimeDistortion family whose rate varies smoothly across the window:
a trapezoid with cosine-eased corners. By default the two ramps meet at the
midpoint (a raised-cosine bump), so the rate eases up from normal speed, peaks,
and eases back out with no kink at the seams. EasedTimeDilation and
EasedTimeCompression are drop-in replacements for the constant pair, taking the
same three arguments.

An optional fourth argument shapes the easing. { ramp } sets both ease regions;
{ rampIn, rampOut } override each side and win over the shorthand, CSS-padding
style (a side of 0 is a hard seam). Narrowing the ramps opens a flat plateau in
the middle, decoupling the peak deviation from the window average so the
slowdown can be sustained and gentle, or asymmetric. Each ramp accepts a
Duration of real time (the natural unit for imperceptibility, an absolute-time
effect) or a bare number as a fraction of the window (handy for a normalized
control); overlapping ramps are clamped, not rejected.

distortTime returns the definite integral of the rate over
[offset, offset + length] -- the difference of a normalized cumulative at the
slice endpoints -- not a point sample, so the offset Clock accumulates stays
independent of polling frequency. The cumulative runs 0 -> 1 for any ramp, so
the whole-window contract is unchanged: over referenceDuration of real time the
fake clock still advances exactly relativeDuration, and composed windows net out
identically to the constant case. Custom curves subclass EasedTimeDistortion and
override cumulativeFraction.

Closes #9

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@boatmeme boatmeme merged commit 8927fa8 into main Jun 28, 2026
4 checks passed
@boatmeme boatmeme deleted the easing branch June 28, 2026 07:47
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.

Add eased (non-constant) distortions for imperceptible rate changes

1 participant