Resources Numerical accuracy Calculation policy

Keys dragged in the graph editor

Turns a drag on the curve editor's canvas into a key's new time and value, or a handle's new offset.

How the precision is kept

A canvas position is mapped linearly to seconds and to the curve's value in 64-bit numbers. A dragged key's time is rounded to 0.001 s before it is stored; its value is stored as computed.

Calculations under this policy

3 calculations in 1 source file. The code is read from the released app when this page loads. It is the calculation people run, not a copy kept for this page.

anim/graph.js ModelCraft 0.8.1 64-bit 3 calculations Reviewed

Tests bin/test-animation.mjs

Canvas position to time and value

64-bit

The time and value under a canvas point.

t = t₀ + (x − pad) ÷ (w − 2 pad) × (t₁ − t₀); v = v₀ + (h − pad − y) ÷ (h − 2 pad) × (v₁ − v₀)

One 64-bit expression each. The result has the resolution of the pointer position: one pixel is (t₁ − t₀) ÷ (w − 2 pad) seconds.

  • anim/graph.js
  • lines 23 to 26
  • ModelCraft 0.8.1
export function fromPx(view, x, y) {
  const p = view.pad ?? 12;
  return [view.t0 + ((x - p) / (view.w - 2 * p)) * (view.t1 - view.t0), view.v0 + ((view.h - p - y) / (view.h - 2 * p)) * (view.v1 - view.v0)];
}

A key dragged to a new time and value

64-bit

Moves a key in time and sets one number of its value.

time = max(0, round(t × 1000) ÷ 1000); value = v

The time is rounded to 0.001 s with Math.round before it is stored; the value is stored unrounded. The key is found again afterward by exact equality with the rounded time.

  • anim/graph.js
  • lines 95 to 108
  • ModelCraft 0.8.1
/** A key dragged to (t, v): its time moves (onto another key's time it replaces it) and that number of its value changes. */
export function dragKey(anim, p, t, v) {
  const tr = trackOf(anim, p.track);
  if (!tr) return anim;
  const k = tr.keys[p.i];
  if (!k) return anim;
  const to = Math.max(0, r3(t));
  let a = Math.abs(to - k.t) > 1e-9 ? moveKey(anim, p.track, k.t, to) : anim;
  const tr2 = trackOf(a, p.track), k2 = tr2 && tr2.keys.find((x) => x.t === to);
  if (!k2) return a;
  const val = k2.v.slice();
  val[p.comp] = v;
  return setPropKey(a, tr2.target, tr2.prop, to, val, k2.ease);
}

A handle dragged

64-bit

The handle's new offset from its key in time and value.

Δt = t − key time (≤ 0 for the left handle, ≥ 0 for the right); Δv = v − key value

Plain 64-bit differences from the key; nothing is rounded here.

  • anim/graph.js
  • lines 109 to 119
  • ModelCraft 0.8.1
/** A handle dragged to (t, v): the other numbers of its key keep their handle values. */
export function dragHandle(anim, p, side, t, v) {
  const tr = trackOf(anim, p.track), k = tr && tr.keys[p.i];
  if (!k) return anim;
  const h = handlePoints(tr, p.i)[side];
  let dt = t - k.t;
  if (side === 'l') dt = Math.min(0, dt); else dt = Math.max(0, dt);
  const dv = h[1].map((x, j) => x - k.v[j]);
  dv[p.comp] = v - k.v[p.comp];
  return setHandle(anim, p.track, k.t, side, [dt, ...dv]);
}

In detail

Method
Linear map between canvas pixels and the view's time and value ranges.
Number types
64-bit numbers.
Tolerances
A key's time is moved only when it differs from the old one by more than 1e-9 s.
Checked by
bin/test-animation.mjs loads the development build's copy of this file and calls fromPx, dragKey and dragHandle.
Limits
Key times set by dragging lie on a 0.001 s grid and are never negative.

Approach: 64-bit, direct

Every value is a 64-bit number from input to result. Formulas are closed form or a direct solve, and every comparison uses a stated tolerance.

Every policy that takes this approach