R360: motion that eases, a drag that coasts, and the owner's switch (#153) - #171
Merged
Conversation
…153) A click on the ring ran at one pace from the first frame to the last and stopped dead on arrival; a drag stopped the instant the hand left the picture. Both have a shape now. A travel gathers pace, runs fastest halfway and settles onto its frame; a drag thrown rather than put down keeps turning at the hand's speed and slows to a stop. The coast is not a second mechanism. `coastAfterDrag` reads the speed of the throw, works out how far constant slowing carries it and how long that takes, and hands those frames to the same travel a ring click uses — so a grab, a key, a frame count that changed under the hook and the #161 landing timer hold a coast exactly as they hold a travel, none of them taught anything new. Nor is its curve a look chosen by eye: 2t − t² IS where constant slowing puts a thing, the same motion the distance was measured with, so the frames and the time cannot drift apart. The owner gets a sixth parameter to turn both off, per work, in works.r360_params — so no migration. It is stored the other way about from the cue points: absent means ON, and only an owner turning it off is ever written down, so every work saved before today glides too. A visitor whose system asks for less motion gets neither, whatever the owner chose. The direction control and the new one are the same two-way toggle with different words in it, so they are one component now. The feel itself — how fast a coast sheds speed, and so how long it runs and how far it carries — is one constant in lib/r360/orbit.ts, to be tried on a phone with Dawid. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
#153. A click on the ring ran at one pace
from the first frame to the last and stopped dead on arrival; a drag stopped the instant the hand
left the picture. Both have a shape now.
on the ring, and a cue point (#107) with it.
slows to a stop.
works.r360_params— so no migration.How it works
The coast is not a second mechanism.
coastAfterDragreads the speed of the throw, works out howfar constant slowing carries it and how long that takes, and hands those frames to the same travel
a ring click uses. So a grab, a key, a frame count that changed under the hook and the
#161 landing timer hold a coast exactly as
they hold a travel, with none of them taught anything new.
Its curve is not a look chosen by eye either.
2t − t²IS where constant slowing puts a thing —the same motion the distance was measured with — so the frames and the time cannot drift apart.
There is no "too slow to coast" threshold to keep in step with the rest: a throw that would not
carry a whole frame simply does not coast, which is the floor stating itself.
The bug review caught, and it was the important one. The speed was measured between pointer
moves, so the time a hand spends resting before it lifts never reached the divisor. The measured
speed stayed exactly what it had been while the hand was still travelling — right up to the moment
the sample window emptied, and then fell to nothing. A finger lifts tens of milliseconds after it
stops, which is the ordinary gesture, so "drag, stop, let go" would have thrown the orbit most of
the way round. Simulated on a 72-frame set: 51 frames of coast after a 60 ms rest, where it should
have been 6.
The fix is that the lift is now a reading like any other, not merely the moment of asking: resting
lengthens the span without lengthening the distance, and the throw drains smoothly to nothing. The
test named "drains the throw as the hand rests, rather than all at once" is that regression.
Two smaller things came out of the same pass. The press is no longer recorded as a reading — it is
where the finger landed, not motion, and counting it measured a speed across the drag slop, so a
tap that wobbled 8 px could throw the orbit. And the readings are stamped with
event.timeStamprather than
performance.now(): on an orbit page the main thread stalls decoding frames, thequeued moves then drain in one task, and handler time would read a whole swipe as having happened
at once.
The switch
Stored the other way about from the cue points: absent means ON, and only an owner turning it
off is ever written down. So every work saved before today glides too,
isDefaultR360Paramsstillreads untouched parameters as untouched, and nothing has to be backfilled.
Open, for Dawid: the issue left "which way a work saved before it reads" to be decided. This
ships it ON, on the grounds that the motion is a quality every orbit should have had rather than
content the owner has to author — and that shipping it off would mean almost nobody ever sees it.
Say the word and it is a one-line change.
To try on the preview
The owner's own preview has a ring, so the feel can be judged in the edit form without saving
anything. What the issue asks to tune on a phone with Dawid — the curve, how long a coast lasts,
and whether a slow release coasts at all — is one constant,
COAST_SLOWINGinsrc/lib/r360/orbit.ts. It sets how fast a coast sheds speed, and so both how long a throw runsand how far it carries.
Two existing e2e drags had to be fixed
dragAcrossine2e/db/r360.spec.tsand the preview drag ine2e/db/works.spec.tsdomouse.down→mouse.move(…, {steps: 4})→mouse.upwith no pause. Playwright lands those moveswithin a millisecond of one another — a speed no hand reaches — so every one of them became a
maximal throw, and the orbit coasted out from under the assertions that follow. The works-form one
would have failed outright:
"Użyj tej klatki"reads the frame in view mid-spin, and the test endson an exact
toEqualof the POST body.Both now come to rest before letting go, which is what those tests always meant by a drag.
dragAcrosstakes{ thrown: true }for the one case that wants a throw.Also
The direction control and the new one are the same two-way toggle with different words in it, so
they are one component (
TwoWayParam) now, and its hint is tied to the buttons witharia-describedby— the legend names the parameter, not what choosing either way does.travelPathinring.tsand the coast both need the frames a turn passes through, so that is onefunction (
framesAlong) now. A travel's pace moved intolib/r360/orbit.tsso the coast's cap canbe derived from
TRAVEL_MAX_MSinstead of repeating 1200 in a second file.Test plan
pnpm check— typecheck, eslint, 655 unit testspnpm build— the App Router pages compilecoastOnRelease), so all five ways of not coastingare tested in the existing node environment: a cancelled pointer, the glide off, reduced
motion, a hand at rest, and a tap that wobbled. The hook keeps only the reading of the clock
and of the event.
one-frame floor — and derive their inputs from the limits, so tuning the feel cannot break them
glidekeyin the saved parameters; a reduced-motion visitor's throw does not coast
the switch off and with reduced motion on
🤖 Generated with Claude Code