drawnui-react 0.1.0-preview.16 → 0.1.0-preview.18
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.
- package/README.md +1 -1
- package/SKIPPED.md +7 -7
- package/dist/controls/AnimatedFramesRenderer.d.ts +7 -0
- package/dist/controls/AnimatedFramesRenderer.js +16 -1
- package/dist/controls/DrawnGame.d.ts +4 -1
- package/dist/controls/DrawnGame.js +7 -3
- package/dist/controls/GridStructure.d.ts +13 -1
- package/dist/controls/GridStructure.js +38 -2
- package/dist/controls/SkiaButton.d.ts +12 -2
- package/dist/controls/SkiaButton.js +19 -6
- package/dist/controls/SkiaCarousel.d.ts +1 -1
- package/dist/controls/SkiaCarousel.js +3 -2
- package/dist/controls/SkiaEditor.d.ts +5 -1
- package/dist/controls/SkiaEditor.js +19 -17
- package/dist/controls/SkiaGif.d.ts +3 -0
- package/dist/controls/SkiaGif.js +11 -1
- package/dist/controls/SkiaImage.js +15 -10
- package/dist/controls/SkiaLabel.d.ts +52 -3
- package/dist/controls/SkiaLabel.js +222 -50
- package/dist/controls/SkiaLayout.d.ts +5 -0
- package/dist/controls/SkiaLayout.js +7 -4
- package/dist/controls/SkiaLottie.js +3 -1
- package/dist/controls/SkiaProgress.d.ts +2 -1
- package/dist/controls/SkiaProgress.js +4 -1
- package/dist/controls/SkiaRadioButton.js +3 -1
- package/dist/controls/SkiaScroll.d.ts +17 -1
- package/dist/controls/SkiaScroll.js +84 -9
- package/dist/controls/SkiaShape.d.ts +13 -2
- package/dist/controls/SkiaShape.js +50 -10
- package/dist/controls/SkiaSlider.d.ts +10 -2
- package/dist/controls/SkiaSlider.js +15 -2
- package/dist/controls/SkiaSprite.d.ts +1 -0
- package/dist/controls/SkiaSprite.js +4 -0
- package/dist/controls/SkiaToggle.d.ts +5 -1
- package/dist/controls/SkiaToggle.js +11 -2
- package/dist/controls/SnappingLayout.d.ts +10 -1
- package/dist/controls/SnappingLayout.js +16 -1
- package/dist/core/Accessibility.d.ts +13 -0
- package/dist/core/Accessibility.js +10 -4
- package/dist/core/Animators.d.ts +28 -1
- package/dist/core/Animators.js +76 -8
- package/dist/core/Canvas.d.ts +21 -4
- package/dist/core/Canvas.js +122 -23
- package/dist/core/Easing.d.ts +12 -1
- package/dist/core/Easing.js +25 -1
- package/dist/core/Gestures.d.ts +24 -1
- package/dist/core/Gestures.js +75 -2
- package/dist/core/Hover.d.ts +14 -4
- package/dist/core/Hover.js +38 -22
- package/dist/core/ScrollAnimators.js +9 -0
- package/dist/core/SkiaControl.d.ts +27 -6
- package/dist/core/SkiaControl.js +81 -38
- package/dist/react/SkiaShell.js +33 -14
- package/dist/react/index.js +29 -2
- package/package.json +16 -1
package/dist/core/Animators.js
CHANGED
|
@@ -16,7 +16,20 @@ export class AnimatorBase {
|
|
|
16
16
|
StartFrameTimeNanos = 0;
|
|
17
17
|
OnStart;
|
|
18
18
|
OnStop;
|
|
19
|
+
/** Paused: the canvas does not tick it and it wakes no frames; Resume goes on from where it was (C# Pause / Resume). */
|
|
20
|
+
IsPaused = false;
|
|
21
|
+
/** Paused by the canvas because its control is hidden; resumed when it shows again. */
|
|
22
|
+
PausedWhileHidden = false;
|
|
23
|
+
/**
|
|
24
|
+
* Frame time (ns) before which nothing it shows changes: the canvas does not tick it and it keeps no frames coming
|
|
25
|
+
* until then, a timer wakes the frame loop at that time (a GIF or a sprite between two frames; DrawnUi.Rust frame
|
|
26
|
+
* animators sleep between changes). 0 = tick every frame.
|
|
27
|
+
*/
|
|
28
|
+
SleepUntilNanos = 0;
|
|
19
29
|
delayHandle = 0;
|
|
30
|
+
delayEnd = 0;
|
|
31
|
+
delayLeft = 0;
|
|
32
|
+
pausedAt = 0;
|
|
20
33
|
constructor(parent) { this.Parent = parent; }
|
|
21
34
|
Register() {
|
|
22
35
|
const parent = this.Parent;
|
|
@@ -60,6 +73,8 @@ export class AnimatorBase {
|
|
|
60
73
|
this.delayHandle = 0;
|
|
61
74
|
}
|
|
62
75
|
if (delayMs > 0) {
|
|
76
|
+
// the delay waits without frames
|
|
77
|
+
this.delayEnd = performance.now() + delayMs;
|
|
63
78
|
this.delayHandle = window.setTimeout(() => { this.delayHandle = 0; this.Start(); }, delayMs);
|
|
64
79
|
return;
|
|
65
80
|
}
|
|
@@ -72,7 +87,43 @@ export class AnimatorBase {
|
|
|
72
87
|
}
|
|
73
88
|
}
|
|
74
89
|
}
|
|
90
|
+
/** Freezes the run (and what is left of a start delay); Resume goes on from there. */
|
|
91
|
+
Pause() {
|
|
92
|
+
if (this.IsPaused)
|
|
93
|
+
return;
|
|
94
|
+
this.IsPaused = true;
|
|
95
|
+
this.pausedAt = performance.now();
|
|
96
|
+
if (this.delayHandle) {
|
|
97
|
+
clearTimeout(this.delayHandle);
|
|
98
|
+
this.delayHandle = 0;
|
|
99
|
+
this.delayLeft = Math.max(0, this.delayEnd - this.pausedAt);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
/** Goes on from where Pause froze it: the paused time is not counted. */
|
|
103
|
+
Resume() {
|
|
104
|
+
if (!this.IsPaused)
|
|
105
|
+
return;
|
|
106
|
+
this.IsPaused = false;
|
|
107
|
+
const paused = (performance.now() - this.pausedAt) * 1_000_000;
|
|
108
|
+
if (this.StartFrameTimeNanos !== 0) {
|
|
109
|
+
this.StartFrameTimeNanos += paused;
|
|
110
|
+
this.LastFrameTimeNanos += paused;
|
|
111
|
+
}
|
|
112
|
+
if (this.SleepUntilNanos !== 0)
|
|
113
|
+
this.SleepUntilNanos += paused;
|
|
114
|
+
if (this.delayLeft > 0) {
|
|
115
|
+
const left = this.delayLeft;
|
|
116
|
+
this.delayLeft = 0;
|
|
117
|
+
this.Start(left);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
this.Parent?.Repaint(); // frames again
|
|
121
|
+
}
|
|
75
122
|
Stop() {
|
|
123
|
+
this.IsPaused = false;
|
|
124
|
+
this.PausedWhileHidden = false;
|
|
125
|
+
this.SleepUntilNanos = 0;
|
|
126
|
+
this.delayLeft = 0;
|
|
76
127
|
if (this.delayHandle) {
|
|
77
128
|
clearTimeout(this.delayHandle);
|
|
78
129
|
this.delayHandle = 0;
|
|
@@ -155,23 +206,21 @@ export class SkiaValueAnimator extends AnimatorBase {
|
|
|
155
206
|
}
|
|
156
207
|
/** Passed over mValue; subclasses derive their own reported value here. */
|
|
157
208
|
TransformReportedValue(_deltaT) { return this.mValue; }
|
|
158
|
-
/**
|
|
209
|
+
/**
|
|
210
|
+
* Updates mValue from elapsed time; true when the run is over. The eased value is not clamped to the range: a spring
|
|
211
|
+
* easing goes past its ends, and a run whose mMinValue is above mMaxValue (a ping-pong's way back) goes down.
|
|
212
|
+
*/
|
|
159
213
|
UpdateValue(_deltaT, deltaFromStart) {
|
|
160
214
|
const elapsedMs = deltaFromStart / 1_000_000;
|
|
161
215
|
const progress = this.Speed > 0 ? elapsedMs / this.Speed : 1;
|
|
162
216
|
const deltaValue = this.mMaxValue - this.mMinValue;
|
|
163
217
|
const eased = this.Easing.Ease(Math.min(progress, 1));
|
|
164
218
|
this.Progress = eased;
|
|
165
|
-
|
|
166
|
-
if (value < this.mMinValue) {
|
|
167
|
-
this.mValue = this.mMinValue;
|
|
168
|
-
return false;
|
|
169
|
-
}
|
|
170
|
-
if (value >= this.mMaxValue || progress >= 1) {
|
|
219
|
+
if (progress >= 1) {
|
|
171
220
|
this.mValue = this.mMaxValue;
|
|
172
221
|
return true;
|
|
173
222
|
}
|
|
174
|
-
this.mValue =
|
|
223
|
+
this.mValue = deltaValue * eased + this.mMinValue;
|
|
175
224
|
return false;
|
|
176
225
|
}
|
|
177
226
|
FinishedRunning() {
|
|
@@ -188,6 +237,25 @@ export class SkiaValueAnimator extends AnimatorBase {
|
|
|
188
237
|
return true;
|
|
189
238
|
}
|
|
190
239
|
}
|
|
240
|
+
/** DrawnUi PingPongAnimator: every repeat runs the other way (Repeat -1 = forever by default). */
|
|
241
|
+
export class PingPongAnimator extends SkiaValueAnimator {
|
|
242
|
+
/** One way done, the next starts (C# CycleFInished). */
|
|
243
|
+
CycleFinished;
|
|
244
|
+
constructor(parent) { super(parent); this.Repeat = -1; }
|
|
245
|
+
FinishedRunning() {
|
|
246
|
+
if (this.Repeat !== 0) {
|
|
247
|
+
this.CycleFinished?.();
|
|
248
|
+
if (this.Repeat > 0)
|
|
249
|
+
this.Repeat--;
|
|
250
|
+
[this.mMinValue, this.mMaxValue] = [this.mMaxValue, this.mMinValue];
|
|
251
|
+
this.mValue = this.mMinValue;
|
|
252
|
+
this.LastFrameTimeNanos = 0;
|
|
253
|
+
this.StartFrameTimeNanos = 0;
|
|
254
|
+
return false;
|
|
255
|
+
}
|
|
256
|
+
return super.FinishedRunning();
|
|
257
|
+
}
|
|
258
|
+
}
|
|
191
259
|
/** DrawnUi RenderingAnimator: a value animator that paints itself above its Parent (IOverlayEffect). */
|
|
192
260
|
export class RenderingAnimator extends SkiaValueAnimator {
|
|
193
261
|
constructor(parent) { super(parent); }
|
package/dist/core/Canvas.d.ts
CHANGED
|
@@ -15,6 +15,11 @@ export declare class Canvas {
|
|
|
15
15
|
readonly Element: HTMLCanvasElement;
|
|
16
16
|
/** Points a pointer may travel between Down and Up and still count as a tap (AppoMobi TouchEffect default). */
|
|
17
17
|
static TappedCancelMoveThresholdPoints: number;
|
|
18
|
+
/**
|
|
19
|
+
* A press held this long without moving past TappedCancelMoveThresholdPoints is a LongPressing, at the press point
|
|
20
|
+
* (AppoMobi TouchEffect, DrawnUi.Rust LONG_PRESS_MS); its release sends no Tapped. A timer, no frames while waiting.
|
|
21
|
+
*/
|
|
22
|
+
static LongPressTimeMs: number;
|
|
18
23
|
BackgroundColor: Color;
|
|
19
24
|
/** Accelerated = WebGL surface, Default = software. Read once at first frame. */
|
|
20
25
|
RenderingMode: RenderingModeType;
|
|
@@ -96,6 +101,10 @@ export declare class Canvas {
|
|
|
96
101
|
UnregisterAnimator(uid: number): void;
|
|
97
102
|
/** Ticks every registered animator once; returns how many ran. */
|
|
98
103
|
protected ExecuteAnimators(frameTimeNanos: number): number;
|
|
104
|
+
private wakeTimer;
|
|
105
|
+
private wakeAtNanos;
|
|
106
|
+
/** One timer for the earliest sleeping animator: the frame loop wakes then, no frames run in between. */
|
|
107
|
+
private WakeAt;
|
|
99
108
|
/** Frames drawn so far (SkiaBackdrop uses it to refresh once after a cached record). */
|
|
100
109
|
FrameIndex: number;
|
|
101
110
|
/**
|
|
@@ -143,10 +152,11 @@ export declare class Canvas {
|
|
|
143
152
|
private cursorPointer;
|
|
144
153
|
/**
|
|
145
154
|
* DrawnUi.Blazor shows `cursor: pointer` over interactive controls through its overlay elements; here the overlay
|
|
146
|
-
* is pointer-events:none, so the
|
|
147
|
-
*
|
|
148
|
-
* (itself tappable, or a tappable span of a label)
|
|
149
|
-
*
|
|
155
|
+
* is pointer-events:none, so the canvas decides from the hover hit path (Hover.Path: the controls under the mouse by
|
|
156
|
+
* the routing rules of ProcessGestures, so nothing below a popup that blocks gestures below): a control in the
|
|
157
|
+
* accessibility tree (it has a role) that answers WantsPointerCursor (itself tappable, or a tappable span of a label)
|
|
158
|
+
* shows the hand. A popup's close-on-background-tap wrapper has no role, so its backdrop shows none. The canvas
|
|
159
|
+
* element's cursor is switched only when the answer changes. Mouse moves only, none in the frame loop.
|
|
150
160
|
*/
|
|
151
161
|
private UpdateCursor;
|
|
152
162
|
/**
|
|
@@ -179,9 +189,16 @@ export declare class Canvas {
|
|
|
179
189
|
private readonly UpdateTouchAction;
|
|
180
190
|
private pageObserver?;
|
|
181
191
|
private AttachInput;
|
|
192
|
+
/** The page's own overscroll-behavior before Lock set it to none (html, body), put back on detach. */
|
|
193
|
+
private pageOverscroll?;
|
|
182
194
|
private DetachInput;
|
|
183
195
|
/** Port of DrawnUi.Blazor Canvas.OnTouchAction: per-pointer state machine producing Down / Panning / Tapped / Up. */
|
|
184
196
|
OnTouchAction(args: TouchActionEventArgs): void;
|
|
197
|
+
private readonly longPressTimers;
|
|
198
|
+
private readonly longPressed;
|
|
199
|
+
/** The long press of a new press: due LongPressTimeMs later unless the pointer moves away or lets go. */
|
|
200
|
+
private ArmLongPress;
|
|
201
|
+
private DisarmLongPress;
|
|
185
202
|
private OnGestureEvent;
|
|
186
203
|
private ProcessPendingGestures;
|
|
187
204
|
/**
|
package/dist/core/Canvas.js
CHANGED
|
@@ -13,6 +13,11 @@ export class Canvas {
|
|
|
13
13
|
Element;
|
|
14
14
|
/** Points a pointer may travel between Down and Up and still count as a tap (AppoMobi TouchEffect default). */
|
|
15
15
|
static TappedCancelMoveThresholdPoints = 16;
|
|
16
|
+
/**
|
|
17
|
+
* A press held this long without moving past TappedCancelMoveThresholdPoints is a LongPressing, at the press point
|
|
18
|
+
* (AppoMobi TouchEffect, DrawnUi.Rust LONG_PRESS_MS); its release sends no Tapped. A timer, no frames while waiting.
|
|
19
|
+
*/
|
|
20
|
+
static LongPressTimeMs = 1500;
|
|
16
21
|
BackgroundColor = Colors.Transparent;
|
|
17
22
|
/** Accelerated = WebGL surface, Default = software. Read once at first frame. */
|
|
18
23
|
RenderingMode = "Accelerated";
|
|
@@ -242,17 +247,51 @@ export class Canvas {
|
|
|
242
247
|
UnregisterAnimator(uid) { this.AnimatingControls.delete(uid); }
|
|
243
248
|
/** Ticks every registered animator once; returns how many ran. */
|
|
244
249
|
ExecuteAnimators(frameTimeNanos) {
|
|
245
|
-
let executed = 0;
|
|
250
|
+
let executed = 0, wake = Infinity;
|
|
246
251
|
for (const a of [...this.AnimatingControls.values()]) {
|
|
247
252
|
if (!a.Parent) {
|
|
248
253
|
this.AnimatingControls.delete(a.Uid);
|
|
249
254
|
continue;
|
|
250
255
|
}
|
|
256
|
+
// the control or a parent is hidden: its animator pauses, no frames, and goes on from where it was when shown
|
|
257
|
+
if (a.Parent.HiddenInTree()) {
|
|
258
|
+
if (!a.IsPaused) {
|
|
259
|
+
a.Pause();
|
|
260
|
+
a.PausedWhileHidden = true;
|
|
261
|
+
}
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
if (a.PausedWhileHidden) {
|
|
265
|
+
a.PausedWhileHidden = false;
|
|
266
|
+
a.Resume();
|
|
267
|
+
}
|
|
268
|
+
if (a.IsPaused)
|
|
269
|
+
continue; // a paused animator neither ticks nor keeps frames coming (C# DrawnView)
|
|
270
|
+
if (a.SleepUntilNanos > frameTimeNanos) {
|
|
271
|
+
wake = Math.min(wake, a.SleepUntilNanos);
|
|
272
|
+
continue;
|
|
273
|
+
} // nothing changes yet
|
|
251
274
|
a.TickFrame(frameTimeNanos);
|
|
252
275
|
executed++;
|
|
276
|
+
if (a.SleepUntilNanos > frameTimeNanos)
|
|
277
|
+
wake = Math.min(wake, a.SleepUntilNanos);
|
|
253
278
|
}
|
|
279
|
+
this.WakeAt(wake, frameTimeNanos);
|
|
254
280
|
return executed;
|
|
255
281
|
}
|
|
282
|
+
wakeTimer = 0;
|
|
283
|
+
wakeAtNanos = Infinity;
|
|
284
|
+
/** One timer for the earliest sleeping animator: the frame loop wakes then, no frames run in between. */
|
|
285
|
+
WakeAt(nanos, nowNanos) {
|
|
286
|
+
if (!isFinite(nanos))
|
|
287
|
+
return;
|
|
288
|
+
if (this.wakeTimer && this.wakeAtNanos <= nanos)
|
|
289
|
+
return;
|
|
290
|
+
if (this.wakeTimer)
|
|
291
|
+
clearTimeout(this.wakeTimer);
|
|
292
|
+
this.wakeAtNanos = nanos;
|
|
293
|
+
this.wakeTimer = window.setTimeout(() => { this.wakeTimer = 0; this.wakeAtNanos = Infinity; this.Update(); }, Math.max(0, (nanos - nowNanos) / 1_000_000));
|
|
294
|
+
}
|
|
256
295
|
/** Frames drawn so far (SkiaBackdrop uses it to refresh once after a cached record). */
|
|
257
296
|
FrameIndex = 0;
|
|
258
297
|
/**
|
|
@@ -311,6 +350,10 @@ export class Canvas {
|
|
|
311
350
|
}
|
|
312
351
|
Dispose() {
|
|
313
352
|
this.disposed = true;
|
|
353
|
+
if (this.wakeTimer) {
|
|
354
|
+
clearTimeout(this.wakeTimer);
|
|
355
|
+
this.wakeTimer = 0;
|
|
356
|
+
}
|
|
314
357
|
this.Element.removeEventListener("webglcontextlost", this.onContextLost);
|
|
315
358
|
this.Element.removeEventListener("webglcontextrestored", this.onContextRestored);
|
|
316
359
|
this.Gestures = "Disabled";
|
|
@@ -351,10 +394,10 @@ export class Canvas {
|
|
|
351
394
|
if (type === "Moved" && !this.activeTouchIds.has(e.pointerId)) {
|
|
352
395
|
if (e.pointerType === "touch")
|
|
353
396
|
return; // touch never hovers
|
|
354
|
-
if (e.pointerType === "mouse")
|
|
355
|
-
this.UpdateCursor(e.offsetX, e.offsetY);
|
|
356
397
|
if (this.activeTouchIds.size === 0)
|
|
357
398
|
this.Hover.Move(this.content, e.offsetX * this.RenderingScale, e.offsetY * this.RenderingScale);
|
|
399
|
+
if (e.pointerType === "mouse")
|
|
400
|
+
this.UpdateCursor();
|
|
358
401
|
return;
|
|
359
402
|
}
|
|
360
403
|
if ((type === "Released" || type === "Cancelled") && !this.activeTouchIds.has(e.pointerId))
|
|
@@ -414,23 +457,19 @@ export class Canvas {
|
|
|
414
457
|
cursorPointer = false;
|
|
415
458
|
/**
|
|
416
459
|
* DrawnUi.Blazor shows `cursor: pointer` over interactive controls through its overlay elements; here the overlay
|
|
417
|
-
* is pointer-events:none, so the
|
|
418
|
-
*
|
|
419
|
-
* (itself tappable, or a tappable span of a label)
|
|
420
|
-
*
|
|
460
|
+
* is pointer-events:none, so the canvas decides from the hover hit path (Hover.Path: the controls under the mouse by
|
|
461
|
+
* the routing rules of ProcessGestures, so nothing below a popup that blocks gestures below): a control in the
|
|
462
|
+
* accessibility tree (it has a role) that answers WantsPointerCursor (itself tappable, or a tappable span of a label)
|
|
463
|
+
* shows the hand. A popup's close-on-background-tap wrapper has no role, so its backdrop shows none. The canvas
|
|
464
|
+
* element's cursor is switched only when the answer changes. Mouse moves only, none in the frame loop.
|
|
421
465
|
*/
|
|
422
|
-
UpdateCursor(
|
|
466
|
+
UpdateCursor() {
|
|
423
467
|
let hit = false;
|
|
424
|
-
const
|
|
425
|
-
for (
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
// the control decides (whole control, or a tappable span of a label); point in pixels relative to the control's
|
|
429
|
-
// origin, taken from the snapshot rect (already carries the scroll / cache offset; a rotated control gets its bbox)
|
|
430
|
-
if (n.Source.WantsPointerCursor((x - n.Rect.Left) * scale, (y - n.Rect.Top) * scale)) {
|
|
468
|
+
const path = this.Hover.Path, xs = this.Hover.PathX, ys = this.Hover.PathY;
|
|
469
|
+
for (let i = path.length - 1; i >= 0 && !hit; i--) {
|
|
470
|
+
const c = path[i];
|
|
471
|
+
if (c.IsAccessibilityElement && c.WantsPointerCursor(xs[i] - c.DrawingRect.Left, ys[i] - c.DrawingRect.Top))
|
|
431
472
|
hit = true;
|
|
432
|
-
break;
|
|
433
|
-
}
|
|
434
473
|
}
|
|
435
474
|
if (hit !== this.cursorPointer) {
|
|
436
475
|
this.cursorPointer = hit;
|
|
@@ -534,11 +573,22 @@ export class Canvas {
|
|
|
534
573
|
el.addEventListener("pointerleave", this.onPointerLeave);
|
|
535
574
|
el.addEventListener("contextmenu", this.onContextMenu);
|
|
536
575
|
el.addEventListener("wheel", this.onWheel, { passive: false });
|
|
537
|
-
if (this.gestures === "Lock")
|
|
576
|
+
if (this.gestures === "Lock") {
|
|
538
577
|
el.addEventListener("touchmove", this.preventTouch, { passive: false });
|
|
578
|
+
// the canvas owns every touch (DrawnUi.Web applyGestureStyle, DrawnUi.Rust host): no iOS callout or text
|
|
579
|
+
// selection on a long press, and no rubber band / pull-down of the page starting on it
|
|
580
|
+
const st = el.style;
|
|
581
|
+
st.webkitUserSelect = "none";
|
|
582
|
+
st.webkitTouchCallout = "none";
|
|
583
|
+
this.pageOverscroll = [document.documentElement.style.overscrollBehavior, document.body.style.overscrollBehavior];
|
|
584
|
+
document.documentElement.style.overscrollBehavior = "none";
|
|
585
|
+
document.body.style.overscrollBehavior = "none";
|
|
586
|
+
}
|
|
539
587
|
else if (this.gestures === "Enabled")
|
|
540
588
|
el.addEventListener("touchmove", this.preventClaimedTouch, { passive: false });
|
|
541
589
|
}
|
|
590
|
+
/** The page's own overscroll-behavior before Lock set it to none (html, body), put back on detach. */
|
|
591
|
+
pageOverscroll;
|
|
542
592
|
DetachInput() {
|
|
543
593
|
const el = this.Element;
|
|
544
594
|
el.style.touchAction = "";
|
|
@@ -553,7 +603,17 @@ export class Canvas {
|
|
|
553
603
|
el.removeEventListener("wheel", this.onWheel);
|
|
554
604
|
el.removeEventListener("touchmove", this.preventTouch);
|
|
555
605
|
el.removeEventListener("touchmove", this.preventClaimedTouch);
|
|
606
|
+
if (this.pageOverscroll) {
|
|
607
|
+
const st = el.style;
|
|
608
|
+
st.webkitUserSelect = "";
|
|
609
|
+
st.webkitTouchCallout = "";
|
|
610
|
+
[document.documentElement.style.overscrollBehavior, document.body.style.overscrollBehavior] = this.pageOverscroll;
|
|
611
|
+
this.pageOverscroll = undefined;
|
|
612
|
+
}
|
|
556
613
|
this.claimedTouches.clear();
|
|
614
|
+
for (const id of [...this.longPressTimers.keys()])
|
|
615
|
+
this.DisarmLongPress(id);
|
|
616
|
+
this.longPressed.clear();
|
|
557
617
|
this.activeTouchIds.clear();
|
|
558
618
|
this.pointerDownArgs.clear();
|
|
559
619
|
this.previousTouchArgs.clear();
|
|
@@ -564,6 +624,7 @@ export class Canvas {
|
|
|
564
624
|
return;
|
|
565
625
|
const id = args.Id;
|
|
566
626
|
if (args.Type === "Pressed") {
|
|
627
|
+
this.ArmLongPress(id, args);
|
|
567
628
|
this.activeTouchIds.add(id);
|
|
568
629
|
args.NumberOfTouches = this.activeTouchIds.size;
|
|
569
630
|
args.StartingLocation = args.Location;
|
|
@@ -588,6 +649,9 @@ export class Canvas {
|
|
|
588
649
|
const downArgs = this.pointerDownArgs.get(id);
|
|
589
650
|
args.StartingLocation = downArgs ? downArgs.StartingLocation : args.Location;
|
|
590
651
|
if (args.Type === "Moved") {
|
|
652
|
+
const threshold = Canvas.TappedCancelMoveThresholdPoints * Math.max(0.1, this.RenderingScale);
|
|
653
|
+
if (Math.abs(args.Distance.Total.X) >= threshold || Math.abs(args.Distance.Total.Y) >= threshold)
|
|
654
|
+
this.DisarmLongPress(id);
|
|
591
655
|
if (args.Distance.Delta.X !== 0 || args.Distance.Delta.Y !== 0)
|
|
592
656
|
this.OnGestureEvent(args, "Panning");
|
|
593
657
|
this.previousTouchArgs.set(id, args);
|
|
@@ -595,7 +659,9 @@ export class Canvas {
|
|
|
595
659
|
}
|
|
596
660
|
if (args.Type === "Released" || args.Type === "Cancelled") {
|
|
597
661
|
args.IsInContact = args.NumberOfTouches > 1;
|
|
598
|
-
|
|
662
|
+
const longPressed = this.longPressed.delete(id);
|
|
663
|
+
this.DisarmLongPress(id);
|
|
664
|
+
if (!args.IsInContact && downArgs && args.Type === "Released" && !longPressed) {
|
|
599
665
|
const threshold = Canvas.TappedCancelMoveThresholdPoints * Math.max(0.1, this.RenderingScale);
|
|
600
666
|
if (Math.abs(args.Distance.Total.X) < threshold && Math.abs(args.Distance.Total.Y) < threshold)
|
|
601
667
|
this.OnGestureEvent(args, "Tapped");
|
|
@@ -606,6 +672,30 @@ export class Canvas {
|
|
|
606
672
|
this.activeTouchIds.delete(id);
|
|
607
673
|
}
|
|
608
674
|
}
|
|
675
|
+
longPressTimers = new Map();
|
|
676
|
+
longPressed = new Set();
|
|
677
|
+
/** The long press of a new press: due LongPressTimeMs later unless the pointer moves away or lets go. */
|
|
678
|
+
ArmLongPress(id, down) {
|
|
679
|
+
this.DisarmLongPress(id);
|
|
680
|
+
this.longPressed.delete(id);
|
|
681
|
+
this.longPressTimers.set(id, window.setTimeout(() => {
|
|
682
|
+
this.longPressTimers.delete(id);
|
|
683
|
+
if (!this.activeTouchIds.has(id))
|
|
684
|
+
return;
|
|
685
|
+
this.longPressed.add(id);
|
|
686
|
+
const args = new TouchActionEventArgs();
|
|
687
|
+
Object.assign(args, down);
|
|
688
|
+
args.Location = down.StartingLocation ?? down.Location;
|
|
689
|
+
this.OnGestureEvent(args, "LongPressing");
|
|
690
|
+
}, Canvas.LongPressTimeMs));
|
|
691
|
+
}
|
|
692
|
+
DisarmLongPress(id) {
|
|
693
|
+
const t = this.longPressTimers.get(id);
|
|
694
|
+
if (t) {
|
|
695
|
+
clearTimeout(t);
|
|
696
|
+
this.longPressTimers.delete(id);
|
|
697
|
+
}
|
|
698
|
+
}
|
|
609
699
|
OnGestureEvent(args, result) {
|
|
610
700
|
this.pendingGestures.push(SkiaGesturesParameters.Create(result, args));
|
|
611
701
|
this.Update();
|
|
@@ -634,21 +724,30 @@ export class Canvas {
|
|
|
634
724
|
/** Entry into the control tree, same shape as DrawnUi Canvas.ProcessGestures. */
|
|
635
725
|
ProcessGestures(root, args) {
|
|
636
726
|
const info = () => new GestureEventProcessingInfo(args.Event.Location, SKPoint.Empty, SKPoint.Empty, null);
|
|
727
|
+
// the wheel goes to the press owner only while a press is held; between presses it goes by position, and it
|
|
728
|
+
// never makes an owner: a wheel over a second scroll moves that one, not the last one the wheel moved
|
|
729
|
+
// (DrawnUi.Rust input_router "the wheel goes to the owner of the press first"; C# keeps the last wheel consumer)
|
|
730
|
+
const wheel = args.Type === "Wheel";
|
|
637
731
|
if (args.Type === "Down")
|
|
638
732
|
this.gestureOwner = null;
|
|
639
|
-
else if (this.gestureOwner && Canvas.IsSavedGesture(args.Type)) {
|
|
733
|
+
else if (this.gestureOwner && Canvas.IsSavedGesture(args.Type) && (!wheel || this.activeTouchIds.size > 0)) {
|
|
640
734
|
const owner = this.gestureOwner;
|
|
641
735
|
const alive = !!owner.Superview && owner.IsVisible && !owner.InputTransparent;
|
|
642
736
|
const consumed = alive ? owner.OnSkiaGestureEvent(args, info()) : null;
|
|
643
737
|
if (consumed) {
|
|
644
738
|
// it still wants the gesture: nobody else sees this one (C# skips the tree pass for a saved gesture)
|
|
645
|
-
|
|
739
|
+
if (!wheel)
|
|
740
|
+
this.gestureOwner = args.Type === "Up" ? null : consumed;
|
|
646
741
|
return consumed;
|
|
647
742
|
}
|
|
648
|
-
|
|
743
|
+
// it let go (a button whose press turned into a pan): route normally again; a wheel it does not use goes by
|
|
744
|
+
// position and leaves it the press
|
|
745
|
+
if (!wheel)
|
|
746
|
+
this.gestureOwner = null;
|
|
649
747
|
}
|
|
650
748
|
const consumed = root.ProcessGestures(args, info());
|
|
651
|
-
|
|
749
|
+
if (!wheel)
|
|
750
|
+
this.gestureOwner = consumed && args.Type !== "Up" ? consumed : null;
|
|
652
751
|
return consumed;
|
|
653
752
|
}
|
|
654
753
|
}
|
package/dist/core/Easing.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/** Mirrors MAUI Easing
|
|
1
|
+
/** Mirrors MAUI Easing: Ease(t) with t in 0..1 (Spring* leave 0..1 on the way). */
|
|
2
2
|
export declare class Easing {
|
|
3
3
|
private readonly fn;
|
|
4
4
|
constructor(fn: (x: number) => number);
|
|
@@ -9,5 +9,16 @@ export declare class Easing {
|
|
|
9
9
|
static readonly CubicInOut: Easing;
|
|
10
10
|
static readonly SinOut: Easing;
|
|
11
11
|
static readonly SinIn: Easing;
|
|
12
|
+
/** Half a cosine: slow at both ends. */
|
|
13
|
+
static readonly SinInOut: Easing;
|
|
14
|
+
/** Leaps to the end, bounces three times and settles. */
|
|
15
|
+
static readonly BounceOut: Easing;
|
|
16
|
+
/** Bounces three times, then leaps to the end. */
|
|
17
|
+
static readonly BounceIn: Easing;
|
|
18
|
+
/** Moves away first, then leaps to the end. */
|
|
19
|
+
static readonly SpringIn: Easing;
|
|
20
|
+
/** Goes a little past the end and comes back. */
|
|
21
|
+
static readonly SpringOut: Easing;
|
|
22
|
+
private static Bounce;
|
|
12
23
|
static readonly Default: Easing;
|
|
13
24
|
}
|
package/dist/core/Easing.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/** Mirrors MAUI Easing
|
|
1
|
+
/** Mirrors MAUI Easing: Ease(t) with t in 0..1 (Spring* leave 0..1 on the way). */
|
|
2
2
|
export class Easing {
|
|
3
3
|
fn;
|
|
4
4
|
constructor(fn) {
|
|
@@ -11,5 +11,29 @@ export class Easing {
|
|
|
11
11
|
static CubicInOut = new Easing((x) => (x < 0.5 ? 4 * x ** 3 : (x - 1) * (2 * x - 2) ** 2 + 1));
|
|
12
12
|
static SinOut = new Easing((x) => Math.sin((x * Math.PI) / 2));
|
|
13
13
|
static SinIn = new Easing((x) => 1 - Math.cos((x * Math.PI) / 2));
|
|
14
|
+
/** Half a cosine: slow at both ends. */
|
|
15
|
+
static SinInOut = new Easing((x) => -Math.cos(Math.PI * x) / 2 + 0.5);
|
|
16
|
+
/** Leaps to the end, bounces three times and settles. */
|
|
17
|
+
static BounceOut = new Easing(Easing.Bounce);
|
|
18
|
+
/** Bounces three times, then leaps to the end. */
|
|
19
|
+
static BounceIn = new Easing((x) => 1 - Easing.Bounce(1 - x));
|
|
20
|
+
/** Moves away first, then leaps to the end. */
|
|
21
|
+
static SpringIn = new Easing((x) => x * x * ((1.70158 + 1) * x - 1.70158));
|
|
22
|
+
/** Goes a little past the end and comes back. */
|
|
23
|
+
static SpringOut = new Easing((x) => (x - 1) * (x - 1) * ((1.70158 + 1) * (x - 1) + 1.70158) + 1);
|
|
24
|
+
static Bounce(p) {
|
|
25
|
+
if (p < 1 / 2.75)
|
|
26
|
+
return 7.5625 * p * p;
|
|
27
|
+
if (p < 2 / 2.75) {
|
|
28
|
+
p -= 1.5 / 2.75;
|
|
29
|
+
return 7.5625 * p * p + 0.75;
|
|
30
|
+
}
|
|
31
|
+
if (p < 2.5 / 2.75) {
|
|
32
|
+
p -= 2.25 / 2.75;
|
|
33
|
+
return 7.5625 * p * p + 0.9375;
|
|
34
|
+
}
|
|
35
|
+
p -= 2.625 / 2.75;
|
|
36
|
+
return 7.5625 * p * p + 0.984375;
|
|
37
|
+
}
|
|
14
38
|
static Default = Easing.CubicInOut;
|
|
15
39
|
}
|
package/dist/core/Gestures.d.ts
CHANGED
|
@@ -9,7 +9,7 @@ export declare class SKPoint {
|
|
|
9
9
|
}
|
|
10
10
|
/** Raw platform action (subset of TouchActionType). */
|
|
11
11
|
export type TouchActionType = "Pressed" | "Moved" | "Released" | "Cancelled" | "Wheel" | "Pointer";
|
|
12
|
-
/** Recognized gesture (TouchActionResult). LongPressing
|
|
12
|
+
/** Recognized gesture (TouchActionResult). LongPressing after Canvas.LongPressTimeMs held still; Pointer / Touch declared for parity, not produced. */
|
|
13
13
|
export type TouchActionResult = "Touch" | "Down" | "Up" | "Tapped" | "LongPressing" | "Panning" | "Wheel" | "Pointer";
|
|
14
14
|
export type GesturesMode = "Disabled" | "Enabled" | "Lock";
|
|
15
15
|
export type LockTouch = "Disabled" | "Enabled" | "PassNone" | "PassTap" | "PassTapAndLongPress";
|
|
@@ -21,6 +21,27 @@ export declare class DistanceInfo {
|
|
|
21
21
|
Total: SKPoint;
|
|
22
22
|
Velocity: SKPoint;
|
|
23
23
|
}
|
|
24
|
+
/**
|
|
25
|
+
* The last positions of a press with their times (DrawnUi.Rust gestures `Trail`, 6d525f7). Measured only against the
|
|
26
|
+
* previous move, irregular delivery made the velocity worthless: moves handled in one burst (queued pointer events
|
|
27
|
+
* handed over back to back by a busy main thread, WSLg pairs 0.01 ms apart) gave millions of px/s and every fling at
|
|
28
|
+
* the limit. Moves under BurstMs apart are one position; a move's velocity is its displacement over the last WindowMs,
|
|
29
|
+
* the earlier position interpolated; when the previous move is already that old, its own velocity exactly (evenly
|
|
30
|
+
* spaced input at 60 Hz measures as before).
|
|
31
|
+
*/
|
|
32
|
+
export declare class VelocityTrail {
|
|
33
|
+
/** A move's velocity is its displacement over this much time. */
|
|
34
|
+
static readonly WindowMs = 16;
|
|
35
|
+
/** Below this since the press no velocity is measured yet: the first moves of a burst. */
|
|
36
|
+
static readonly MinMs = 4;
|
|
37
|
+
/** Moves closer than this came in one burst: one position, at the burst's first time. */
|
|
38
|
+
static readonly BurstMs = 1;
|
|
39
|
+
private static readonly Capacity;
|
|
40
|
+
private readonly points;
|
|
41
|
+
Push(x: number, y: number, t: number): void;
|
|
42
|
+
/** The velocity at (x, y) at time t (ms), pixels per second; `fallback` right after the press. */
|
|
43
|
+
Velocity(x: number, y: number, t: number, fallback: SKPoint): SKPoint;
|
|
44
|
+
}
|
|
24
45
|
/** AppoMobi.Gestures MouseButton: which button pressed / released (DOM button 0..4). */
|
|
25
46
|
export type MouseButton = "Left" | "Middle" | "Right" | "XButton1" | "XButton2" | "Extended";
|
|
26
47
|
export type PointerDeviceType = "Mouse" | "Touch" | "Pen";
|
|
@@ -49,6 +70,8 @@ export declare class TouchActionEventArgs {
|
|
|
49
70
|
Timestamp: number;
|
|
50
71
|
/** ms since the previous event of the same pointer. */
|
|
51
72
|
DeltaTimeMs: number;
|
|
73
|
+
/** The press's recent positions the velocity is measured over, carried from event to event of one pointer. */
|
|
74
|
+
Trail?: VelocityTrail;
|
|
52
75
|
/**
|
|
53
76
|
* Mouse wheel: Delta > 0 = wheel down / right (browser sign). IsHorizontal: the event is along the X axis (a tilting
|
|
54
77
|
* wheel, the sideways part of a two-finger touchpad swipe), as C# WheelEventArgs.IsHorizontal.
|
package/dist/core/Gestures.js
CHANGED
|
@@ -18,6 +18,67 @@ export class DistanceInfo {
|
|
|
18
18
|
Total = SKPoint.Empty;
|
|
19
19
|
Velocity = SKPoint.Empty;
|
|
20
20
|
}
|
|
21
|
+
/**
|
|
22
|
+
* The last positions of a press with their times (DrawnUi.Rust gestures `Trail`, 6d525f7). Measured only against the
|
|
23
|
+
* previous move, irregular delivery made the velocity worthless: moves handled in one burst (queued pointer events
|
|
24
|
+
* handed over back to back by a busy main thread, WSLg pairs 0.01 ms apart) gave millions of px/s and every fling at
|
|
25
|
+
* the limit. Moves under BurstMs apart are one position; a move's velocity is its displacement over the last WindowMs,
|
|
26
|
+
* the earlier position interpolated; when the previous move is already that old, its own velocity exactly (evenly
|
|
27
|
+
* spaced input at 60 Hz measures as before).
|
|
28
|
+
*/
|
|
29
|
+
export class VelocityTrail {
|
|
30
|
+
/** A move's velocity is its displacement over this much time. */
|
|
31
|
+
static WindowMs = 16;
|
|
32
|
+
/** Below this since the press no velocity is measured yet: the first moves of a burst. */
|
|
33
|
+
static MinMs = 4;
|
|
34
|
+
/** Moves closer than this came in one burst: one position, at the burst's first time. */
|
|
35
|
+
static BurstMs = 1;
|
|
36
|
+
static Capacity = 16;
|
|
37
|
+
points = [];
|
|
38
|
+
Push(x, y, t) {
|
|
39
|
+
const last = this.points[this.points.length - 1];
|
|
40
|
+
if (last && t - last.T < VelocityTrail.BurstMs) {
|
|
41
|
+
last.X = x;
|
|
42
|
+
last.Y = y;
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
if (this.points.length === VelocityTrail.Capacity)
|
|
46
|
+
this.points.shift();
|
|
47
|
+
this.points.push({ X: x, Y: y, T: t });
|
|
48
|
+
}
|
|
49
|
+
/** The velocity at (x, y) at time t (ms), pixels per second; `fallback` right after the press. */
|
|
50
|
+
Velocity(x, y, t, fallback) {
|
|
51
|
+
let n = this.points.length;
|
|
52
|
+
// a move in the burst of the last position measures from the positions before it
|
|
53
|
+
if (n > 0 && t - this.points[n - 1].T < VelocityTrail.BurstMs)
|
|
54
|
+
n--;
|
|
55
|
+
if (n === 0)
|
|
56
|
+
return fallback;
|
|
57
|
+
const since = t - this.points[0].T;
|
|
58
|
+
if (since < VelocityTrail.MinMs)
|
|
59
|
+
return fallback;
|
|
60
|
+
const window = Math.min(VelocityTrail.WindowMs, since), at = t - window;
|
|
61
|
+
const last = this.points[n - 1];
|
|
62
|
+
if (last.T <= at) {
|
|
63
|
+
const secs = (t - last.T) / 1000;
|
|
64
|
+
return new SKPoint((x - last.X) / secs, (y - last.Y) / secs);
|
|
65
|
+
}
|
|
66
|
+
// the position at `at`: between the two positions around it (the current one included)
|
|
67
|
+
let fromX = this.points[0].X, fromY = this.points[0].Y, prev = this.points[0];
|
|
68
|
+
for (let i = 1; i <= n; i++) {
|
|
69
|
+
const p = i < n ? this.points[i] : { X: x, Y: y, T: t };
|
|
70
|
+
if (p.T >= at) {
|
|
71
|
+
const share = p.T - prev.T < 1e-6 ? 1 : (at - prev.T) / (p.T - prev.T);
|
|
72
|
+
fromX = prev.X + (p.X - prev.X) * share;
|
|
73
|
+
fromY = prev.Y + (p.Y - prev.Y) * share;
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
prev = p;
|
|
77
|
+
}
|
|
78
|
+
const secs = window / 1000;
|
|
79
|
+
return new SKPoint((x - fromX) / secs, (y - fromY) / secs);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
21
82
|
/** AppoMobi.Gestures PointerData: the device and button behind a Down / Up / Tapped (every button is delivered). */
|
|
22
83
|
export class PointerData {
|
|
23
84
|
Button = "Left";
|
|
@@ -43,6 +104,8 @@ export class TouchActionEventArgs {
|
|
|
43
104
|
Timestamp = performance.now();
|
|
44
105
|
/** ms since the previous event of the same pointer. */
|
|
45
106
|
DeltaTimeMs = 0;
|
|
107
|
+
/** The press's recent positions the velocity is measured over, carried from event to event of one pointer. */
|
|
108
|
+
Trail;
|
|
46
109
|
/**
|
|
47
110
|
* Mouse wheel: Delta > 0 = wheel down / right (browser sign). IsHorizontal: the event is along the X axis (a tilting
|
|
48
111
|
* wheel, the sideways part of a two-finger touchpad swipe), as C# WheelEventArgs.IsHorizontal.
|
|
@@ -68,8 +131,18 @@ export class TouchActionEventArgs {
|
|
|
68
131
|
d.End = released ? previous.Location : current.Location;
|
|
69
132
|
d.Delta = released ? SKPoint.Empty : current.Location.Subtract(previous.Location);
|
|
70
133
|
d.Total = previous.Distance.Total.Add(d.Delta);
|
|
71
|
-
|
|
72
|
-
|
|
134
|
+
if (released)
|
|
135
|
+
d.Velocity = previous.Distance.Velocity;
|
|
136
|
+
else {
|
|
137
|
+
let trail = previous.Trail;
|
|
138
|
+
if (!trail) {
|
|
139
|
+
trail = new VelocityTrail();
|
|
140
|
+
trail.Push(previous.Location.X, previous.Location.Y, previous.Timestamp);
|
|
141
|
+
}
|
|
142
|
+
d.Velocity = trail.Velocity(current.Location.X, current.Location.Y, current.Timestamp, previous.Distance.Velocity);
|
|
143
|
+
trail.Push(current.Location.X, current.Location.Y, current.Timestamp);
|
|
144
|
+
current.Trail = trail;
|
|
145
|
+
}
|
|
73
146
|
current.Distance = d;
|
|
74
147
|
}
|
|
75
148
|
}
|