@jsenv/navi 0.29.175 → 0.29.176

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,40 @@ 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
+ - **the other axis, over a faded stream, is not ours to swallow.** Within a
151
+ push, cross-axis events are the push's own wobble and are eaten with the
152
+ rest — a hand is never perfectly straight. But once our axis is momentum
153
+ only, a hand pushing the other way is a NEW gesture, and on an axis the box
154
+ does not travel it is not ours at all: it is handed back to whatever scrolls
155
+ under the pointer, untouched, and it does not renew the claim either. Renewed
156
+ by what it swallows, the gesture would be kept alive by the very scroll it
157
+ ignores — every attempt extending the silence it was waiting for. Handed
158
+ back, though, only when it IS a scroll: at the end of a fade the deltas are a
159
+ pixel or two on each axis, and read alone such an event says "the other
160
+ axis" as easily as it says nothing — passed along, those crumbs scroll the
161
+ slide's own content a few pixels under the travel, a header creeping off the
162
+ edge of the box. Three things separate a scroll from the crumbs, and all
163
+ three are asked for: the other axis clearly dominates the event (the same
164
+ bar a drag uses to give up its press); the event is the size of a hand, or
165
+ the run is growing event over event — a scroll ramping up from rest, which
166
+ decay never looks like; and it is a RUN — wobble is isolated, a scroll
167
+ insists (`WHEEL_CROSS_RUN`, `WHEEL_CROSS_CONFIRM_DELTA`). The run costs a
168
+ real scroll its first event or two — a few pixels, one notch of a stepped
169
+ wheel — which is the cheap side of the trade.
170
+
86
171
  Taking it is also the only way to stop the browser from answering it: on a
87
172
  laptop a horizontal two-finger swipe IS the back-navigation gesture, and a
88
173
  region that neither takes it nor lets it go is the worst of the three — the page
@@ -442,11 +527,29 @@ hold:
442
527
  moves are read from the window, filtered by pointer id, so nothing is missed
443
528
  for not owning the pointer.
444
529
 
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.
530
+ And the refusal has to be _listenable_ **before the finger lands**, even though
531
+ it only refuses later: whether a touchmove can be refused at all is decided
532
+ when the touch begins, from the non-passive listeners present at that moment.
533
+ Registered at the pointerdown it is already too late — the events arrive
534
+ `cancelable: false`, the listener runs, calls `preventDefault`, and nothing
535
+ happens. What that costs is not the start of the gesture but its middle: the
536
+ travel takes the press fine (the axis the browser was left never moved), and
537
+ then the thumb's arc bends towards the axis `touch-action` leaves to the page —
538
+ the browser starts the page's own scroll and CANCELS the pointer stream, and
539
+ the screen the hand was carrying snaps back with the finger still down.
540
+
541
+ So every box that travels says it from its own render, next to the attribute
542
+ naming its axes: `onTouchMove={keepTouchRefusable}` — a no-op listener whose
543
+ being registered is the whole of it (the same function everything picked up
544
+ and carried registers, see `keepTouchRefusable` in drag_after_intent.js). A
545
+ JSX prop is enough because the passive-by-default intervention only covers
546
+ `window`, `document` and `document.body`: on any other element a plain
547
+ listener is non-passive already — which matters, because a framework's event
548
+ prop cannot say `passive: false`. The one place the explicit option is
549
+ load-bearing is a listener that goes down on the document root (see
550
+ drag_to.js). And a surface whose swipe starts only from a grip carrying
551
+ `touch-action: none` needs none of this: the browser was never offered
552
+ anything there to take back.
450
553
 
451
554
  ### A navi component that reads the pointer marks ITSELF
452
555
 
@@ -552,3 +655,40 @@ dropped, and those things are left where they are, live: the bar jumps to the
552
655
  tab one is heading for instead of sliding to a tab one is not. A slide would be
553
656
  nicer, and it is not available — the browser measured both of its ends before
554
657
  the hand changed its mind, and neither can be asked for again.
658
+
659
+ ## Verifying a gesture
660
+
661
+ A gesture is verified the way movement is (see the animations skill,
662
+ "Verifying") — driven synthetically, read as numbers — but what has to be
663
+ synthesized is the HAND'S imperfection, because that is where gesture bugs
664
+ live. A gesture simulated as clean, evenly-spaced points along one axis passes
665
+ forever and proves nothing:
666
+
667
+ - **coalesce**: deliver a whole flick in one or two moves — that is what a
668
+ fast thumb looks like to the main thread;
669
+ - **arc**: lean the first points off-axis the way a thumb does, and only then
670
+ commit to the axis;
671
+ - **interleave**: start the second gesture while the first one's travel — or
672
+ its momentum tail — is still playing, and sweep the delay between the two;
673
+ - **for a wheel**: a swipe is a ramp, a peak and a decaying tail at frame
674
+ rate, and the interesting cases live in the tail — a second swipe over it, a
675
+ cross-axis scroll into it, decay jitter that must not step.
676
+
677
+ The reading side has traps of its own:
678
+
679
+ - coordinates measured off an element that is mid-travel are stale by dispatch
680
+ time — aim at where the CONTAINER is, never at where the content was;
681
+ - a scripted gesture's wall-clock is diluted by driver round-trips, so
682
+ measured velocities come out below the numbers written in the script — read
683
+ the measured ones, not the intended ones;
684
+ - what the real browser does with an unclaimed touch — take it for a pan and
685
+ cancel the stream, kill a momentum tail the moment a finger lands — does not
686
+ exist in emulation at all. Claims about it are only ever settled on a
687
+ device (and the Firefox wheel limitation above is the same kind of fact).
688
+
689
+ What to read while it runs: the `navi_drag_*` events (grab, start, release)
690
+ say which stage a press reached and with what velocity; the box's state
691
+ attribute (`data-slide-current`, the URL) says what came of it; the track's
692
+ measured position, sampled across frames, says what the screen did in
693
+ between — including the mini-movements and stalls no end-state check ever
694
+ 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.176",
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.42",
33
33
  "@jsenv/humanize": "1.7.8",
34
34
  "@jsenv/validity": "0.4.3"
35
35
  },