@jsenv/navi 0.29.175 → 0.29.177

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -16,21 +16,35 @@ and are referenced from here rather than restated.
16
16
  ## What the rules are
17
17
 
18
18
  - A press is not a gesture until it has wandered ~10px, and the axis it leans on
19
- then is the axis it walks, for good. **Except on something already moving**:
20
- there the hand said what it wanted by reaching for it, so the gesture answers
21
- from its first pixel and owes it every one of them — asking it to cross a
22
- threshold is asking twice, and over those pixels the thing it is holding
23
- answers to nobody. A diagonal would ask for two travels at
19
+ then is the axis it walks, for good — how that axis is read, and why the
20
+ reading is biased, has [a section of its own
21
+ below](#the-axis-is-read-once-and-read-with-a-bias). **Except on something
22
+ already moving**: there the hand said what it wanted by reaching for it, so
23
+ the gesture answers from its first pixel and owes it every one of them —
24
+ asking it to cross a threshold is asking twice, and over those pixels the
25
+ thing it is holding answers to nobody. A diagonal would ask for two travels at
24
26
  once and only one screen can arrive.
25
- - Letting go carries on when about a third of a box has been pulled, or on a
26
- flick, whatever the distance — a gesture that has clearly begun is an
27
- intention, and asking for a screen to be dragged all the way across turns a
28
- travel into work.
29
- - **A hand still moving says where it is going, and it says it both ways.** Away
30
- from what was being brought in, it means "put it back" whatever the distance
31
- already covered — otherwise a screen caught in flight and thrown back still
32
- arrives, because the picture alone decided. A slow nudge is not a throw: there
33
- the distance decides, as usual.
27
+ - **Of the way covered when the gesture forms, only the threshold is withheld**
28
+ — never the whole first report. Movement reports are coalesced to frames, so
29
+ the faster the gesture the bigger its first report is: charged whole, a flick
30
+ delivered in one report sets off with nothing left of itself to have pulled —
31
+ it moves nothing on screen and is refused at the release for it, and what the
32
+ hand feels is a swipe that did strictly nothing.
33
+ - **A hand still moving when it lets go says where this goes, and it says it
34
+ both ways.** Towards what it was bringing in, the travel carries on — however
35
+ slowly and whatever the distance: an intention still being acted on at the
36
+ lift. Away from it, fast, everything goes back whatever the distance already
37
+ covered — otherwise a screen caught in flight and thrown back still arrives,
38
+ because the picture alone decided. Only a release at rest, or a slow retreat,
39
+ is judged by the picture — about a third of a box — because there the
40
+ position is the only witness left.
41
+ - **The bars those verdicts compare against sit well below the speed the
42
+ fingertip actually has, and they must.** Velocity is averaged over a trailing
43
+ window (pointer events arrive irregularly, and the last one before a release
44
+ often repeats its coordinates — measured on that pair alone, every throw ends
45
+ at zero), and the release itself adds one more sample at the same place a
46
+ moment later. Both pull the measure down from the hand's peak: a threshold
47
+ sized against the hand refuses the hand.
34
48
  - Pulling towards nothing follows the finger at a fraction of its distance and
35
49
  comes back: a wall one can lean on, never walk through.
36
50
  - **A direction that will be refused is one of those walls, and it must be a
@@ -52,6 +66,43 @@ and are referenced from here rather than restated.
52
66
  hand that came in fast would push against a screen that does not answer for as
53
67
  many pixels as it went too far.
54
68
 
69
+ ## The axis is read once, and read with a bias
70
+
71
+ The axis is decided on the first movement report after the threshold and never
72
+ revisited. That report is ~10-25px of movement, and for a thumb those pixels
73
+ misstate the gesture: a thumb swiping sideways moves along an ARC, and the
74
+ start of the arc leans off-axis far more than the swipe does. Read even
75
+ (whichever axis covered more), the lean hands the whole press to an axis
76
+ nobody meant — and a press given up is given up WHOLE: the hand's remaining
77
+ hundred pixels are read by no one, and what it feels is a swipe that did
78
+ nothing at all.
79
+
80
+ So on a box that travels one axis, the reading is biased towards that axis:
81
+ the cross axis takes the press only when it clearly dominates the first
82
+ report (2×, `AXIS_CROSS_DOMINANCE`). A gesture that really belongs to the
83
+ other axis — a scroll — is near-pure on its axis from the first pixel (4-6×),
84
+ so it still leaves whole, at once, in the same tick. A box that travels both
85
+ axes keeps the even reading: every answer is a travel there, and there is
86
+ nothing to protect.
87
+
88
+ Why the decision cannot simply wait for more evidence — this boundary is
89
+ physics, not caution:
90
+
91
+ - the browser is racing for the same press, on the axis `touch-action` leaves
92
+ it, and it holds off only while the touchmoves are being refused;
93
+ - refusing a touchmove is **irreversible for that touch**: whether the stream
94
+ can scroll is decided at its first refusable move, and un-refusing later
95
+ resurrects nothing;
96
+ - so every frame spent gathering evidence is a frame of the page's scroll
97
+ spent, for good. A decision deferred until the arc has proven itself would
98
+ make every genuinely vertical swipe over the box a dead gesture — the exact
99
+ bug, mirrored onto the page.
100
+
101
+ The bias moves the frame-one boundary to where the two hands actually
102
+ separate; nothing can remove the boundary. A first report steeper than the
103
+ bias (a start more than twice off-axis) is genuinely ambiguous with a scroll
104
+ and goes to the page — which at least answers it visibly.
105
+
55
106
  ## Two inputs, one travel
56
107
 
57
108
  A thumb dragging the page and a wheel pushing it sideways both ask to travel,
@@ -83,6 +134,39 @@ Two things say "the hand asked again", and a stream only ever has one of them:
83
134
  decay — is therefore one step, and a second flick over the tail of the first
84
135
  is heard as its own.
85
136
 
137
+ Both are read against a stream that never announces itself: the browser sees
138
+ one unbroken burst (the tail of the first push never went silent), so "the
139
+ hand asked again" is reconstructed, never received. Two consequences are easy
140
+ to get wrong from there:
141
+
142
+ - **a second push is answered like a first event — a screen, now** — never with
143
+ credit towards one. The price a screen after the first costs (an accumulated
144
+ delta, deliberately steep) exists to keep momentum from walking slides on its
145
+ own; charged to a hand that pushed again over the tail, it reads as "my
146
+ swipes are ignored until I wait for silence". The stream itself says which is
147
+ which: once it has faded into a recognized tail, growing **twice in a row**
148
+ is a hand (`WHEEL_REGROW_RUN`) — decay jitter bumps up in isolated events and
149
+ never twice consecutively.
150
+ - **cross-axis events are swallowed absolutely, and they renew nothing.** A
151
+ hand is never perfectly straight, so events leaning the other way are the
152
+ gesture's own wobble and are eaten with the rest — and there the swallowing
153
+ has to be total, because no per-event reading tells a scroll's onset from
154
+ that wobble. It was tried, and every filter leaks: end-of-fade crumbs are a
155
+ pixel or two landing on either axis (a `(0,1)` event is "clearly the other
156
+ axis" by any dominance test), a slightly diagonal swipe's tail carries
157
+ hand-sized deltas on both axes, and one crumb let through scrolls the
158
+ slide's own content under the travel — a header creeping off the edge of the
159
+ box, one pixel per leak. Do not re-attempt a "hand it back when it is really
160
+ a scroll" heuristic; the honest boundary is elsewhere: a swallowed
161
+ cross-axis event does NOT renew the claim. Silence is the only end a wheel
162
+ gesture has, and it is silence ON ITS AXIS — renewed by what it eats, the
163
+ gesture would outlive its own stream, and a hand starting a scroll over the
164
+ tail would extend, event by event, the very deadness it was waiting out.
165
+ Left to lapse, the claim dies shortly after the travel axis goes quiet (on a
166
+ system where touching the surface kills the old momentum, that is moments
167
+ after the new scroll begins), and the browser answers the rest of the scroll
168
+ itself.
169
+
86
170
  Taking it is also the only way to stop the browser from answering it: on a
87
171
  laptop a horizontal two-finger swipe IS the back-navigation gesture, and a
88
172
  region that neither takes it nor lets it go is the worst of the three — the page
@@ -442,11 +526,29 @@ hold:
442
526
  moves are read from the window, filtered by pointer id, so nothing is missed
443
527
  for not owning the pointer.
444
528
 
445
- And the refusal has to be _listened for_ from the grab, even though it only
446
- refuses later: whether a touchmove can be refused at all is decided when the
447
- touch begins, from the non-passive listeners present at that moment. Registered
448
- afterwards, the listener is handed events that are already `cancelable: false` —
449
- it runs, it calls `preventDefault`, and nothing happens.
529
+ And the refusal has to be _listenable_ **before the finger lands**, even though
530
+ it only refuses later: whether a touchmove can be refused at all is decided
531
+ when the touch begins, from the non-passive listeners present at that moment.
532
+ Registered at the pointerdown it is already too late — the events arrive
533
+ `cancelable: false`, the listener runs, calls `preventDefault`, and nothing
534
+ happens. What that costs is not the start of the gesture but its middle: the
535
+ travel takes the press fine (the axis the browser was left never moved), and
536
+ then the thumb's arc bends towards the axis `touch-action` leaves to the page —
537
+ the browser starts the page's own scroll and CANCELS the pointer stream, and
538
+ the screen the hand was carrying snaps back with the finger still down.
539
+
540
+ So every box that travels says it from its own render, next to the attribute
541
+ naming its axes: `onTouchMove={keepTouchRefusable}` — a no-op listener whose
542
+ being registered is the whole of it (the same function everything picked up
543
+ and carried registers, see `keepTouchRefusable` in drag_after_intent.js). A
544
+ JSX prop is enough because the passive-by-default intervention only covers
545
+ `window`, `document` and `document.body`: on any other element a plain
546
+ listener is non-passive already — which matters, because a framework's event
547
+ prop cannot say `passive: false`. The one place the explicit option is
548
+ load-bearing is a listener that goes down on the document root (see
549
+ drag_to.js). And a surface whose swipe starts only from a grip carrying
550
+ `touch-action: none` needs none of this: the browser was never offered
551
+ anything there to take back.
450
552
 
451
553
  ### A navi component that reads the pointer marks ITSELF
452
554
 
@@ -552,3 +654,40 @@ dropped, and those things are left where they are, live: the bar jumps to the
552
654
  tab one is heading for instead of sliding to a tab one is not. A slide would be
553
655
  nicer, and it is not available — the browser measured both of its ends before
554
656
  the hand changed its mind, and neither can be asked for again.
657
+
658
+ ## Verifying a gesture
659
+
660
+ A gesture is verified the way movement is (see the animations skill,
661
+ "Verifying") — driven synthetically, read as numbers — but what has to be
662
+ synthesized is the HAND'S imperfection, because that is where gesture bugs
663
+ live. A gesture simulated as clean, evenly-spaced points along one axis passes
664
+ forever and proves nothing:
665
+
666
+ - **coalesce**: deliver a whole flick in one or two moves — that is what a
667
+ fast thumb looks like to the main thread;
668
+ - **arc**: lean the first points off-axis the way a thumb does, and only then
669
+ commit to the axis;
670
+ - **interleave**: start the second gesture while the first one's travel — or
671
+ its momentum tail — is still playing, and sweep the delay between the two;
672
+ - **for a wheel**: a swipe is a ramp, a peak and a decaying tail at frame
673
+ rate, and the interesting cases live in the tail — a second swipe over it, a
674
+ cross-axis scroll into it, decay jitter that must not step.
675
+
676
+ The reading side has traps of its own:
677
+
678
+ - coordinates measured off an element that is mid-travel are stale by dispatch
679
+ time — aim at where the CONTAINER is, never at where the content was;
680
+ - a scripted gesture's wall-clock is diluted by driver round-trips, so
681
+ measured velocities come out below the numbers written in the script — read
682
+ the measured ones, not the intended ones;
683
+ - what the real browser does with an unclaimed touch — take it for a pan and
684
+ cancel the stream, kill a momentum tail the moment a finger lands — does not
685
+ exist in emulation at all. Claims about it are only ever settled on a
686
+ device (and the Firefox wheel limitation above is the same kind of fact).
687
+
688
+ What to read while it runs: the `navi_drag_*` events (grab, start, release)
689
+ say which stage a press reached and with what velocity; the box's state
690
+ attribute (`data-slide-current`, the URL) says what came of it; the track's
691
+ measured position, sampled across frames, says what the screen did in
692
+ between — including the mini-movements and stalls no end-state check ever
693
+ sees.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsenv/navi",
3
- "version": "0.29.175",
3
+ "version": "0.29.177",
4
4
  "type": "module",
5
5
  "description": "Library of components including navigation to create frontend applications",
6
6
  "repository": {
@@ -29,7 +29,7 @@
29
29
  "prepublishOnly": "npm run build"
30
30
  },
31
31
  "dependencies": {
32
- "@jsenv/dom": "0.17.41",
32
+ "@jsenv/dom": "0.17.43",
33
33
  "@jsenv/humanize": "1.7.8",
34
34
  "@jsenv/validity": "0.4.3"
35
35
  },