Skip to content

The orbit's easing, tuned by the owner — and cue buttons that answer the press, not the arrival #175

Description

@Devski

Wanted by Dawid on 12.09.2026, on top of #153 (PR #171). Three things, all
about how the orbit's motion is controlled and how the cue points answer a
press. Named together because they are one feel, not three fixes.

1. The easing is a parameter, not a switch

Today (#153): motion is a two-way toggle per work — Eased or Constant
(paramGlide in the dictionaries, stored in works.r360_params). The curve
itself is fixed in the code.

Wanted: the owner sets how much it eases, and ease-in and ease-out are
separate
— a travel may gather pace gently and land sharply, or the other
way round. Dawid's words: expose a parameter for the acceleration of the
movement, ease-in and ease-out in particular.

Two things follow:

2. A click on the navigation ring eases — and so does every other travel

Settled with Dawid, 12.09.2026: the click he means is the one on the
navigation ring, not on the picture. The picture stays what it is — a
surface to drag, and a tap opens the enlarged view; nothing there starts a
travel, and nothing should.

Which makes most of this a check rather than a build. In PR #171 the
easing lives inside travelAlong itself (use-orbit.ts calls
run(path, travelDuration(path.length), glide ? "eased" : "steady")), and
every travel in the product goes through that one call:

  • a click on the ring (orbit-ring.tsx),
  • a click on a cue marker on the ring,
  • a press on a cue button under the picture (orbit-cues.tsx:92).

So all three inherit it. What is owed here is not new motion but the proof
that it is one motion: a test that a travel started from a button eases the
same as one started from the ring, so that the day someone gives the buttons
a path of their own they cannot quietly give them a feel of their own.

3. The cue buttons

They light on the press, not on the arrival

Today (src/components/ui/orbit-cues.tsx:80): a button's solid state is
cue.frame === orbit.frame — it is a readout of where the orbit is. So a
press sends the orbit, and the button only lights when the orbit gets there,
a second later.

Wanted: pressing a cue lights it at once, and the orbit then runs to it.
The button reports what was asked for, not what has arrived.

This means the row needs a second piece of state — the cue being travelled to
— and a rule for what happens when a hand interrupts the travel (a drag, a
key): the lit button should stop claiming a destination the orbit is no longer
going to.

The cursor

Today every button in this application shows the plain arrow. Tailwind 4's
preflight (verified in node_modules/tailwindcss/preflight.css, 4.3.3) no
longer sets cursor: pointer on button, and the shared Button base class
(src/components/ui/button.tsx:8) sets only disabled:cursor-not-allowed.
cursor-pointer appears in src/ on a handful of labels and the ring's SVG,
and nowhere else.

Wanted: a cue button feels like a button under the mouse. Worth fixing
where it belongs — the shared base and the cue row — rather than on this one
row, because it is the same everywhere: nothing in the product currently says
"this is pressable" to a mouse.

Passing through a cue must not make it blink

Today: because the solid state is cue.frame === orbit.frame, a travel
that crosses a cue's frame snaps that button into the solid tone for a single
frame and out again. At 28 ms a frame that is a blink.

Wanted (Dawid): it may pulse, gently, as the orbit goes past — it must not
flash.

Constraint, carried from #107 and written next to the code: the row
deliberately has no colour transition, because a fade between the two tones
passes through colours that cannot be read, and axe caught exactly that
mid-way in the lightbox. So a pulse has to stay legible the whole way — it
cannot simply be transition-colors between the rest and the solid tone. Some
other property (a ring, a scale, an opacity that keeps contrast) is the way in.

Verification

  • The owner sets ease-in and ease-out on a work and a visitor feels the
    difference; a work saved before them is unchanged
  • A travel eases the same whether it was started from the ring, a ring
    marker or a button
  • Pressing a cue button lights it immediately, before the orbit moves; a
    hand taking hold on the way clears that claim
  • The cursor over a cue button (and over the app's buttons generally) is
    the pointer
  • An orbit passing a cue does not blink it; whatever it does instead
    passes axe at every point of the animation, in both tones, light and
    dark
  • prefers-reduced-motion still wins over all of it (A13)

Dependencies

Starts after PR #171 closes — Dawid's sequencing, 12.09.2026. That pull
request introduces the motion and the switch; this turns the switch into
parameters and rests on the travelAlong it leaves behind, so it is a next
step rather than a change to it. Related: #107 (the cue points and the reason the row has no colour
transition), #173 (the other motion Dawid asked for, on dragging a list).

Spec: SPEC.md A13 · Labels: ux

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    uxUser experience: everything between the person and the product. Sequenced, never optional.

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions