drawnui-react 0.1.0-preview.2 → 0.1.0-preview.21
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 +17 -4
- package/SKIPPED.md +33 -27
- package/dist/controls/AnimatedFramesRenderer.d.ts +7 -0
- package/dist/controls/AnimatedFramesRenderer.js +16 -1
- package/dist/controls/DrawnGame.d.ts +40 -0
- package/dist/controls/DrawnGame.js +64 -0
- package/dist/controls/GridStructure.d.ts +16 -2
- package/dist/controls/GridStructure.js +40 -4
- package/dist/controls/SkiaBackdrop.d.ts +8 -2
- package/dist/controls/SkiaBackdrop.js +48 -8
- package/dist/controls/SkiaButton.d.ts +19 -2
- package/dist/controls/SkiaButton.js +41 -11
- package/dist/controls/SkiaCarousel.d.ts +9 -1
- package/dist/controls/SkiaCarousel.js +16 -3
- package/dist/controls/SkiaCheckbox.d.ts +0 -1
- package/dist/controls/SkiaCheckbox.js +0 -6
- package/dist/controls/SkiaDrawer.d.ts +2 -0
- package/dist/controls/SkiaDrawer.js +2 -0
- package/dist/controls/SkiaEditor.d.ts +12 -1
- package/dist/controls/SkiaEditor.js +46 -19
- package/dist/controls/SkiaGif.d.ts +3 -0
- package/dist/controls/SkiaGif.js +12 -2
- package/dist/controls/SkiaImage.js +15 -10
- package/dist/controls/SkiaLabel.d.ts +54 -3
- package/dist/controls/SkiaLabel.js +235 -52
- package/dist/controls/SkiaLabelFps.d.ts +16 -0
- package/dist/controls/SkiaLabelFps.js +41 -0
- package/dist/controls/SkiaLayout.d.ts +64 -7
- package/dist/controls/SkiaLayout.js +316 -19
- package/dist/controls/SkiaLottie.js +4 -2
- package/dist/controls/SkiaProgress.d.ts +2 -1
- package/dist/controls/SkiaProgress.js +6 -3
- package/dist/controls/SkiaRadioButton.js +4 -3
- package/dist/controls/SkiaScroll.d.ts +35 -5
- package/dist/controls/SkiaScroll.js +233 -23
- package/dist/controls/SkiaScrollBar.d.ts +27 -0
- package/dist/controls/SkiaScrollBar.js +51 -2
- package/dist/controls/SkiaShaderCarousel.d.ts +5 -0
- package/dist/controls/SkiaShaderCarousel.js +5 -0
- package/dist/controls/SkiaShape.d.ts +13 -2
- package/dist/controls/SkiaShape.js +50 -10
- package/dist/controls/SkiaSlider.d.ts +20 -2
- package/dist/controls/SkiaSlider.js +59 -5
- package/dist/controls/SkiaSprite.d.ts +1 -0
- package/dist/controls/SkiaSprite.js +4 -0
- package/dist/controls/SkiaSvg.js +1 -1
- package/dist/controls/SkiaSwitch.d.ts +0 -1
- package/dist/controls/SkiaSwitch.js +0 -6
- package/dist/controls/SkiaToggle.d.ts +25 -5
- package/dist/controls/SkiaToggle.js +58 -5
- package/dist/controls/SnappingLayout.d.ts +12 -1
- package/dist/controls/SnappingLayout.js +19 -1
- package/dist/core/Accessibility.d.ts +80 -0
- package/dist/core/Accessibility.js +365 -8
- package/dist/core/Animators.d.ts +37 -1
- package/dist/core/Animators.js +102 -9
- package/dist/core/Canvas.d.ts +108 -7
- package/dist/core/Canvas.js +363 -35
- package/dist/core/Easing.d.ts +12 -1
- package/dist/core/Easing.js +25 -1
- package/dist/core/Gestures.d.ts +48 -2
- package/dist/core/Gestures.js +95 -3
- package/dist/core/Hover.d.ts +47 -0
- package/dist/core/Hover.js +134 -0
- package/dist/core/KeyboardManager.d.ts +13 -0
- package/dist/core/KeyboardManager.js +18 -0
- package/dist/core/ScrollAnimators.js +9 -0
- package/dist/core/SkiaControl.d.ts +216 -7
- package/dist/core/SkiaControl.js +516 -59
- package/dist/core/SkiaEffect.d.ts +17 -0
- package/dist/core/SkiaEffect.js +43 -8
- package/dist/core/SkiaImageManager.js +1 -1
- package/dist/core/Styles.d.ts +51 -0
- package/dist/core/Styles.js +92 -0
- package/dist/core/Super.d.ts +17 -1
- package/dist/core/Super.js +35 -4
- package/dist/core/ViewsAdapter.d.ts +7 -2
- package/dist/core/ViewsAdapter.js +10 -3
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/react/SkiaShell.js +33 -14
- package/dist/react/index.d.ts +15 -4
- package/dist/react/index.js +102 -15
- package/dist/react/reconciler.js +34 -12
- package/dist/vite/index.d.ts +9 -0
- package/dist/vite/index.js +36 -3
- package/package.json +33 -3
package/dist/core/Canvas.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { Super } from "./Super";
|
|
2
2
|
import { SkiaAccessibilityManager } from "./Accessibility";
|
|
3
|
+
import { HoverManager } from "./Hover";
|
|
3
4
|
import { Colors, SKRect } from "./Types";
|
|
4
|
-
import { ContextMenuEventArgs, GestureEventProcessingInfo, SKPoint, SkiaGesturesParameters, TouchActionEventArgs, } from "./Gestures";
|
|
5
|
+
import { ContextMenuEventArgs, GestureEventProcessingInfo, PointerData, SKPoint, SkiaGesturesParameters, TouchActionEventArgs, } from "./Gestures";
|
|
5
6
|
/**
|
|
6
7
|
* Mirrors DrawnUi Canvas (DrawnView): hosts one Content control on an HTML canvas element,
|
|
7
8
|
* owns RenderingScale (devicePixelRatio), the surface, the on-demand frame loop and raw input.
|
|
@@ -12,6 +13,11 @@ export class Canvas {
|
|
|
12
13
|
Element;
|
|
13
14
|
/** Points a pointer may travel between Down and Up and still count as a tap (AppoMobi TouchEffect default). */
|
|
14
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;
|
|
15
21
|
BackgroundColor = Colors.Transparent;
|
|
16
22
|
/** Accelerated = WebGL surface, Default = software. Read once at first frame. */
|
|
17
23
|
RenderingMode = "Accelerated";
|
|
@@ -21,8 +27,22 @@ export class Canvas {
|
|
|
21
27
|
/** Frames per second over the last second of drawn frames. */
|
|
22
28
|
FPS = 0;
|
|
23
29
|
frameTimes = [];
|
|
30
|
+
/** DrawnView.WasRendered: a frame has been drawn on a ready surface (set after the first one completes). */
|
|
31
|
+
WasRendered = false;
|
|
32
|
+
/** DrawnView.CanRender: the surface (and its GPU context when accelerated) is ready to draw. */
|
|
33
|
+
get CanRender() { return !!this.surface && !this.disposed; }
|
|
34
|
+
/**
|
|
35
|
+
* DrawnView.WillFirstTimeDraw: raised once, right before the first frame drawn on a ready surface, with that
|
|
36
|
+
* frame's drawing context. The engine draws its first frame while it is being constructed (so the page never shows
|
|
37
|
+
* an unpainted canvas): pass it through the constructor's `init` to receive that frame.
|
|
38
|
+
*/
|
|
39
|
+
WillFirstTimeDraw;
|
|
40
|
+
/** DrawnView.WasDrawn: raised after every frame has been drawn (the C# event passes no context). */
|
|
41
|
+
WasDrawn;
|
|
24
42
|
/** Registry + rate-limited snapshot of accessible controls, rendered by the DOM overlay (DrawnUi AccessibilityManager). */
|
|
25
43
|
AccessibilityManager = new SkiaAccessibilityManager();
|
|
44
|
+
/** Mouse hover of the controls (drawnui-cross 6m): ReceivesHover / IsHovered / HoverChanged. */
|
|
45
|
+
Hover = new HoverManager();
|
|
26
46
|
content;
|
|
27
47
|
get Content() { return this.content; }
|
|
28
48
|
set Content(value) {
|
|
@@ -33,6 +53,7 @@ export class Canvas {
|
|
|
33
53
|
value.Parent = undefined;
|
|
34
54
|
value._superview = this;
|
|
35
55
|
}
|
|
56
|
+
this.Hover.RequestCheck();
|
|
36
57
|
this.Update();
|
|
37
58
|
}
|
|
38
59
|
surface;
|
|
@@ -42,17 +63,28 @@ export class Canvas {
|
|
|
42
63
|
frameId = 0;
|
|
43
64
|
disposed = false;
|
|
44
65
|
observer;
|
|
45
|
-
|
|
66
|
+
/** The WebGL context is lost: no frames until the browser restores it. */
|
|
67
|
+
contextLost = false;
|
|
68
|
+
/**
|
|
69
|
+
* Bumped when the WebGL context was restored: every GPU object made before (Image caches, picture caches that may
|
|
70
|
+
* replay them, kept composite surfaces, effect textures) is remade at its next draw (DrawnUi.Rust Gpu::epoch).
|
|
71
|
+
*/
|
|
72
|
+
GpuEpoch = 0;
|
|
73
|
+
/** `init` runs before the first frame is drawn: the place to attach `WillFirstTimeDraw` / `WasDrawn` for that frame. */
|
|
74
|
+
constructor(Element, init) {
|
|
46
75
|
this.Element = Element;
|
|
47
76
|
if (!Super.CK)
|
|
48
77
|
throw new Error("DrawnUi: call Super.UseDrawnUi()...BuildAsync() before creating a Canvas");
|
|
78
|
+
init?.(this);
|
|
79
|
+
Element.addEventListener("webglcontextlost", this.onContextLost);
|
|
80
|
+
Element.addEventListener("webglcontextrestored", this.onContextRestored);
|
|
49
81
|
this.observer = new ResizeObserver(() => this.OnResized());
|
|
50
82
|
this.observer.observe(Element);
|
|
51
83
|
this.OnResized();
|
|
52
84
|
}
|
|
53
85
|
/** Request a redraw (full measure + arrange + render) on the next animation frame. */
|
|
54
86
|
Update() {
|
|
55
|
-
if (this.frameId || !this.surface || this.disposed)
|
|
87
|
+
if (this.frameId || !this.surface || this.disposed || this.contextLost)
|
|
56
88
|
return;
|
|
57
89
|
const surface = this.surface;
|
|
58
90
|
this.frameId = surface.requestAnimationFrame((c) => {
|
|
@@ -103,16 +135,53 @@ export class Canvas {
|
|
|
103
135
|
this.RenderingScale = dpr;
|
|
104
136
|
this.Element.width = w;
|
|
105
137
|
this.Element.height = h;
|
|
138
|
+
if (this.contextLost)
|
|
139
|
+
return; // the restore makes the surface at this size
|
|
106
140
|
this.ReleaseSurface();
|
|
107
141
|
this.surface = this.CreateSurface(w, h);
|
|
108
142
|
if (!this.surface)
|
|
109
143
|
throw new Error("DrawnUi: cannot create surface");
|
|
110
144
|
this.DrawNow();
|
|
111
145
|
}
|
|
146
|
+
/** WebGL context lost: keep the browser restoring it (preventDefault) and stop the frames. */
|
|
147
|
+
onContextLost = (e) => {
|
|
148
|
+
e.preventDefault();
|
|
149
|
+
this.contextLost = true;
|
|
150
|
+
if (this.frameId) {
|
|
151
|
+
cancelAnimationFrame(this.frameId);
|
|
152
|
+
this.frameId = 0;
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
/**
|
|
156
|
+
* WebGL context restored (as DrawnUi.Rust and the C# Wasm / Blazor heads): every GL object of the lost generation is
|
|
157
|
+
* gone. The old Skia context is abandoned first, while its own GL handle is current and before the new context makes
|
|
158
|
+
* any object, so its GL calls reach only lost-generation objects. CanvasKit exposes no `abandonContext`;
|
|
159
|
+
* `releaseResourcesAndAbandonContext` is its only way to abandon, and after it freeing an old image or surface makes no
|
|
160
|
+
* GL call. Then the same WebGL object is registered again (the old handle deleted first, which clears the canvas'
|
|
161
|
+
* context object) with a new GrContext and surface; GpuEpoch makes the old generation's GPU objects remake themselves.
|
|
162
|
+
*/
|
|
163
|
+
onContextRestored = () => {
|
|
164
|
+
if (this.disposed)
|
|
165
|
+
return;
|
|
166
|
+
if (this.grContext)
|
|
167
|
+
this.grContext.releaseResourcesAndAbandonContext();
|
|
168
|
+
this.surface?.delete();
|
|
169
|
+
this.surface = undefined;
|
|
170
|
+
this.grContext?.delete();
|
|
171
|
+
this.grContext = undefined;
|
|
172
|
+
if (this.glHandle) {
|
|
173
|
+
Super.CK.deleteContext(this.glHandle);
|
|
174
|
+
this.glHandle = undefined;
|
|
175
|
+
}
|
|
176
|
+
this.contextLost = false;
|
|
177
|
+
this.GpuEpoch++;
|
|
178
|
+
this.surface = this.CreateSurface(this.Element.width, this.Element.height);
|
|
179
|
+
this.DrawNow();
|
|
180
|
+
};
|
|
112
181
|
/** Draws one frame immediately (outside the rAF loop) and presents it. */
|
|
113
182
|
DrawNow() {
|
|
114
183
|
const surface = this.surface;
|
|
115
|
-
if (!surface || this.disposed)
|
|
184
|
+
if (!surface || this.disposed || this.contextLost)
|
|
116
185
|
return;
|
|
117
186
|
this.Draw(surface.getCanvas());
|
|
118
187
|
surface.flush();
|
|
@@ -121,7 +190,16 @@ export class Canvas {
|
|
|
121
190
|
Draw(canvas) {
|
|
122
191
|
const started = performance.now();
|
|
123
192
|
this.ProcessPendingGestures();
|
|
124
|
-
|
|
193
|
+
// Animators run on the frame's vsync time (inside a rAF callback document.timeline.currentTime is the frame
|
|
194
|
+
// timestamp), not on when this callback happened to start: callback starts drift by several ms around vsync
|
|
195
|
+
// (measured 12..23 ms between frames shown 16.7 ms apart), and a game loop moving by speed * delta then
|
|
196
|
+
// jumps unevenly on screen.
|
|
197
|
+
const frameTime = Number(document.timeline?.currentTime ?? started);
|
|
198
|
+
this.FrameTimeNanos = Math.round(frameTime * 1_000_000);
|
|
199
|
+
const executed = this.ExecuteAnimators(Math.round(frameTime * 1_000_000));
|
|
200
|
+
const canRender = this.CanRender;
|
|
201
|
+
if (canRender && !this.WasRendered)
|
|
202
|
+
this.WillFirstTimeDraw?.(this, { Canvas: canvas, Surface: this.surface });
|
|
125
203
|
canvas.clear(Super.ParseColor(this.BackgroundColor));
|
|
126
204
|
const root = this.content;
|
|
127
205
|
if (root) {
|
|
@@ -132,6 +210,7 @@ export class Canvas {
|
|
|
132
210
|
root.Render({ Context: { Canvas: canvas, Surface: this.surface }, Destination: new SKRect(0, 0, w, h), Scale: scale });
|
|
133
211
|
}
|
|
134
212
|
this.AccessibilityManager.OnFrameEnd(this.RenderingScale, this.Element.width, this.Element.height, () => this.Update());
|
|
213
|
+
this.Hover.AfterFrame(this.content, this.activeTouchIds.size > 0);
|
|
135
214
|
const now = performance.now();
|
|
136
215
|
this.FrameTime = now - started;
|
|
137
216
|
this.FrameIndex++;
|
|
@@ -141,6 +220,10 @@ export class Canvas {
|
|
|
141
220
|
this.FPS = this.frameTimes.length;
|
|
142
221
|
if (executed > 0)
|
|
143
222
|
this.Update(); // animators running: keep frames coming
|
|
223
|
+
// C# order: WasDrawn at the end of the frame, then the frame counts as rendered
|
|
224
|
+
this.WasDrawn?.(this);
|
|
225
|
+
if (!this.WasRendered && canRender)
|
|
226
|
+
this.WasRendered = true;
|
|
144
227
|
}
|
|
145
228
|
// ---- deferred disposal (DrawnUi DisposeObject: never delete Skia objects mid-frame) ----
|
|
146
229
|
disposeQueue = [];
|
|
@@ -164,19 +247,61 @@ export class Canvas {
|
|
|
164
247
|
UnregisterAnimator(uid) { this.AnimatingControls.delete(uid); }
|
|
165
248
|
/** Ticks every registered animator once; returns how many ran. */
|
|
166
249
|
ExecuteAnimators(frameTimeNanos) {
|
|
167
|
-
let executed = 0;
|
|
250
|
+
let executed = 0, wake = Infinity;
|
|
168
251
|
for (const a of [...this.AnimatingControls.values()]) {
|
|
169
252
|
if (!a.Parent) {
|
|
170
253
|
this.AnimatingControls.delete(a.Uid);
|
|
171
254
|
continue;
|
|
172
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
|
|
173
274
|
a.TickFrame(frameTimeNanos);
|
|
174
275
|
executed++;
|
|
276
|
+
if (a.SleepUntilNanos > frameTimeNanos)
|
|
277
|
+
wake = Math.min(wake, a.SleepUntilNanos);
|
|
175
278
|
}
|
|
279
|
+
this.WakeAt(wake, frameTimeNanos);
|
|
176
280
|
return executed;
|
|
177
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
|
+
}
|
|
178
295
|
/** Frames drawn so far (SkiaBackdrop uses it to refresh once after a cached record). */
|
|
179
296
|
FrameIndex = 0;
|
|
297
|
+
/**
|
|
298
|
+
* DrawnView.WheelDeltaPerNotch: the Wheel.Delta one mouse-wheel notch produces, 100 CSS pixels in Chrome and Edge. A
|
|
299
|
+
* scroll moves by the event's share of a notch, so a touchpad, which sends many small events, scrolls as far as the
|
|
300
|
+
* fingers moved. 0 = units unknown: every event scrolls one line.
|
|
301
|
+
*/
|
|
302
|
+
WheelDeltaPerNotch = 100;
|
|
303
|
+
/** Vsync time of the last frame the animators ran on (nanoseconds, the performance.now() timeline). */
|
|
304
|
+
FrameTimeNanos = 0;
|
|
180
305
|
/** On-screen surface (SkiaBackdrop snapshots it). */
|
|
181
306
|
get Surface() { return this.surface; }
|
|
182
307
|
/**
|
|
@@ -225,6 +350,12 @@ export class Canvas {
|
|
|
225
350
|
}
|
|
226
351
|
Dispose() {
|
|
227
352
|
this.disposed = true;
|
|
353
|
+
if (this.wakeTimer) {
|
|
354
|
+
clearTimeout(this.wakeTimer);
|
|
355
|
+
this.wakeTimer = 0;
|
|
356
|
+
}
|
|
357
|
+
this.Element.removeEventListener("webglcontextlost", this.onContextLost);
|
|
358
|
+
this.Element.removeEventListener("webglcontextrestored", this.onContextRestored);
|
|
228
359
|
this.Gestures = "Disabled";
|
|
229
360
|
this.observer.disconnect();
|
|
230
361
|
this.content?.Dispose();
|
|
@@ -260,17 +391,17 @@ export class Canvas {
|
|
|
260
391
|
e.type === "pointercancel" ? "Cancelled" : undefined;
|
|
261
392
|
if (!type)
|
|
262
393
|
return;
|
|
263
|
-
// only the primary mouse button is a touch (DrawnUi.Blazor): right / middle buttons never start a gesture, and
|
|
264
|
-
// their Up / Cancel (pointer never pressed here) is ignored too, so a right click cannot end as a Tapped
|
|
265
|
-
if (type === "Pressed" && e.pointerType === "mouse" && e.button !== 0)
|
|
266
|
-
return;
|
|
267
394
|
if (type === "Moved" && !this.activeTouchIds.has(e.pointerId)) {
|
|
395
|
+
if (e.pointerType === "touch")
|
|
396
|
+
return; // touch never hovers
|
|
397
|
+
if (this.activeTouchIds.size === 0)
|
|
398
|
+
this.Hover.Move(this.content, e.offsetX * this.RenderingScale, e.offsetY * this.RenderingScale);
|
|
268
399
|
if (e.pointerType === "mouse")
|
|
269
|
-
this.UpdateCursor(
|
|
400
|
+
this.UpdateCursor();
|
|
270
401
|
return;
|
|
271
|
-
}
|
|
402
|
+
}
|
|
272
403
|
if ((type === "Released" || type === "Cancelled") && !this.activeTouchIds.has(e.pointerId))
|
|
273
|
-
return;
|
|
404
|
+
return; // Up of a pointer that never pressed here
|
|
274
405
|
// Capture so Up outside the element still arrives; throws for synthetic events (tests) — harmless.
|
|
275
406
|
if (type === "Pressed") {
|
|
276
407
|
try {
|
|
@@ -284,9 +415,29 @@ export class Canvas {
|
|
|
284
415
|
args.Type = type;
|
|
285
416
|
args.Scale = this.RenderingScale;
|
|
286
417
|
args.Location = new SKPoint((e.clientX - rect.left) * this.RenderingScale, (e.clientY - rect.top) * this.RenderingScale);
|
|
418
|
+
// every mouse button is delivered with its PointerData (AppoMobi.Gestures): a right click is a Down / Up / Tapped
|
|
419
|
+
// with Button "Right" for games and custom controls, and a ContextMenu on top
|
|
420
|
+
const pd = new PointerData();
|
|
421
|
+
pd.Button = e.button === 0 ? "Left" : e.button === 1 ? "Middle" : e.button === 2 ? "Right" : e.button === 3 ? "XButton1" : e.button === 4 ? "XButton2" : "Extended";
|
|
422
|
+
pd.ButtonNumber = e.button === 1 ? 3 : e.button === 2 ? 2 : e.button + 1;
|
|
423
|
+
pd.DeviceType = e.pointerType === "touch" ? "Touch" : e.pointerType === "pen" ? "Pen" : "Mouse";
|
|
424
|
+
pd.PressedButtons = e.buttons;
|
|
425
|
+
args.Pointer = pd;
|
|
287
426
|
this.OnTouchAction(args);
|
|
427
|
+
if (type === "Released" || type === "Cancelled") {
|
|
428
|
+
this.claimedTouches.delete(e.pointerId);
|
|
429
|
+
this.UpdateTouchAction();
|
|
430
|
+
}
|
|
288
431
|
};
|
|
432
|
+
/** The mouse left the canvas: hover ends at once, also while content moves. */
|
|
433
|
+
onPointerLeave = (e) => { if (e.pointerType !== "touch")
|
|
434
|
+
this.Hover.Leave(); };
|
|
289
435
|
preventTouch = (e) => e.preventDefault();
|
|
436
|
+
/** Gestures="Enabled": touch / pen pointers whose Down a control used (see OnTouchAction). */
|
|
437
|
+
claimedTouches = new Set();
|
|
438
|
+
/** Gestures="Enabled": while a claimed touch is down the page may not take its drag. */
|
|
439
|
+
preventClaimedTouch = (e) => { if (this.claimedTouches.size > 0 && e.cancelable)
|
|
440
|
+
e.preventDefault(); };
|
|
290
441
|
/** Called when no control handled a context-menu request; return true to suppress the browser's canvas menu. */
|
|
291
442
|
ContextMenu;
|
|
292
443
|
/** DOM contextmenu (right click / long press / Menu key) -> ContextMenuEventArgs routed through the tree like a tap. */
|
|
@@ -306,32 +457,33 @@ export class Canvas {
|
|
|
306
457
|
cursorPointer = false;
|
|
307
458
|
/**
|
|
308
459
|
* DrawnUi.Blazor shows `cursor: pointer` over interactive controls through its overlay elements; here the overlay
|
|
309
|
-
* is pointer-events:none, so the
|
|
310
|
-
*
|
|
311
|
-
* (itself tappable, or a tappable span of a label)
|
|
312
|
-
*
|
|
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.
|
|
313
465
|
*/
|
|
314
|
-
UpdateCursor(
|
|
466
|
+
UpdateCursor() {
|
|
315
467
|
let hit = false;
|
|
316
|
-
const
|
|
317
|
-
for (
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
// the control decides (whole control, or a tappable span of a label); point in pixels relative to the control's
|
|
321
|
-
// origin, taken from the snapshot rect (already carries the scroll / cache offset; a rotated control gets its bbox)
|
|
322
|
-
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))
|
|
323
472
|
hit = true;
|
|
324
|
-
break;
|
|
325
|
-
}
|
|
326
473
|
}
|
|
327
474
|
if (hit !== this.cursorPointer) {
|
|
328
475
|
this.cursorPointer = hit;
|
|
329
476
|
this.Element.style.cursor = hit ? "pointer" : "";
|
|
330
477
|
}
|
|
331
478
|
}
|
|
332
|
-
/**
|
|
479
|
+
/**
|
|
480
|
+
* Mouse wheel -> TouchActionResult.Wheel.
|
|
481
|
+
* Lock keeps every wheel on the canvas. Enabled shares it with the page, like DrawnUi.Blazor's
|
|
482
|
+
* TouchHandlingStyle.Manual: the page scrolls unless a control consumed this wheel. The browser needs that answer
|
|
483
|
+
* inside this handler, so the wheel is processed now instead of at the next frame (Blazor does the same, for the
|
|
484
|
+
* same reason); pending gestures are flushed first so the event order is kept.
|
|
485
|
+
*/
|
|
333
486
|
onWheel = (e) => {
|
|
334
|
-
e.preventDefault();
|
|
335
487
|
const rect = this.Element.getBoundingClientRect();
|
|
336
488
|
const args = new TouchActionEventArgs();
|
|
337
489
|
args.Id = -1;
|
|
@@ -339,29 +491,129 @@ export class Canvas {
|
|
|
339
491
|
args.Scale = this.RenderingScale;
|
|
340
492
|
args.Location = new SKPoint((e.clientX - rect.left) * this.RenderingScale, (e.clientY - rect.top) * this.RenderingScale);
|
|
341
493
|
args.StartingLocation = args.Location;
|
|
342
|
-
|
|
343
|
-
|
|
494
|
+
// lines and pages to pixels, then the dominant axis (the .NET browser heads, fc8980c1); a vertical scroll leaves
|
|
495
|
+
// horizontal events alone (C# ba03cb20)
|
|
496
|
+
const factor = e.deltaMode === 1 ? 40 : e.deltaMode === 2 ? 800 : 1;
|
|
497
|
+
const horizontal = Math.abs(e.deltaX) > Math.abs(e.deltaY);
|
|
498
|
+
args.Wheel = { Delta: (horizontal ? e.deltaX : e.deltaY) * factor, IsHorizontal: horizontal };
|
|
499
|
+
if (this.gestures === "Lock") {
|
|
500
|
+
e.preventDefault();
|
|
501
|
+
this.OnGestureEvent(args, "Wheel");
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
if (this.ProcessGestureNow(args, "Wheel"))
|
|
505
|
+
e.preventDefault();
|
|
506
|
+
};
|
|
507
|
+
/**
|
|
508
|
+
* Processes one gesture immediately (after whatever is queued) and reports whether a control USED it: a consumer
|
|
509
|
+
* that marked the event Handled, or any consumer that is not a `BlockGesturesBelow` layer. A blocker returns itself
|
|
510
|
+
* for every gesture it keeps from the controls below (a shell page, a popup backdrop), which is not a use: counting
|
|
511
|
+
* it would block the page wheel over every shell app.
|
|
512
|
+
*/
|
|
513
|
+
ProcessGestureNow(args, result) {
|
|
514
|
+
this.ProcessPendingGestures();
|
|
515
|
+
const root = this.content;
|
|
516
|
+
const consumed = root ? this.ProcessGestures(root, SkiaGesturesParameters.Create(result, args)) : null;
|
|
517
|
+
this.Update();
|
|
518
|
+
return !!consumed && (args.Handled || !consumed.BlockGesturesBelow);
|
|
519
|
+
}
|
|
520
|
+
/**
|
|
521
|
+
* The page axes a touch pan over the canvas hands to the browser: "pan-y" when the page (or a scrolling ancestor) can
|
|
522
|
+
* scroll vertically, "pan-x" horizontally, both, or "none". Mirrors MAUI's Gestures="Enabled" inside a native scroll
|
|
523
|
+
* view, where the parent takes the pans it scrolls in.
|
|
524
|
+
*/
|
|
525
|
+
static PageScrollAxes(el) {
|
|
526
|
+
const scrolls = (v) => v === "auto" || v === "scroll" || v === "overlay";
|
|
527
|
+
const clips = (v) => v === "hidden" || v === "clip";
|
|
528
|
+
let x = false, y = false;
|
|
529
|
+
for (let n = el.parentElement; n && n !== document.body && n !== document.documentElement; n = n.parentElement) {
|
|
530
|
+
const st = getComputedStyle(n);
|
|
531
|
+
if (!y && scrolls(st.overflowY) && n.scrollHeight > n.clientHeight + 1)
|
|
532
|
+
y = true;
|
|
533
|
+
if (!x && scrolls(st.overflowX) && n.scrollWidth > n.clientWidth + 1)
|
|
534
|
+
x = true;
|
|
535
|
+
}
|
|
536
|
+
// the viewport scrolls unless html or body clips it
|
|
537
|
+
const root = document.scrollingElement ?? document.documentElement;
|
|
538
|
+
const hs = getComputedStyle(document.documentElement), bs = getComputedStyle(document.body);
|
|
539
|
+
if (!y && !clips(hs.overflowY) && !clips(bs.overflowY) && root.scrollHeight > root.clientHeight + 1)
|
|
540
|
+
y = true;
|
|
541
|
+
if (!x && !clips(hs.overflowX) && !clips(bs.overflowX) && root.scrollWidth > root.clientWidth + 1)
|
|
542
|
+
x = true;
|
|
543
|
+
// a canvas filling a framed document (a widget in an iframe): the page that scrolls is the parent, which a
|
|
544
|
+
// cross-origin frame cannot inspect, so assume it scrolls vertically; horizontal pans and taps stay on the canvas
|
|
545
|
+
if (!y && window.self !== window.top)
|
|
546
|
+
y = true;
|
|
547
|
+
return x && y ? "pan-x pan-y" : y ? "pan-y" : x ? "pan-x" : "none";
|
|
548
|
+
}
|
|
549
|
+
/**
|
|
550
|
+
* Gestures="Enabled" shares touch pans with the page like MAUI's Enabled inside a native scroll view: along an axis
|
|
551
|
+
* the page can scroll, the browser takes the pan (and sends pointercancel), taps and the other axis stay on the
|
|
552
|
+
* canvas. A page that cannot scroll, and Lock, keep every touch. The browser reads touch-action when a touch starts,
|
|
553
|
+
* so it is kept current ahead of time: on attach, when the window / html / body resize, and after every touch.
|
|
554
|
+
*/
|
|
555
|
+
UpdateTouchAction = () => {
|
|
556
|
+
if (this.gestures === "Disabled")
|
|
557
|
+
return;
|
|
558
|
+
const pan = this.gestures === "Enabled" ? Canvas.PageScrollAxes(this.Element) : "none";
|
|
559
|
+
if (this.Element.style.touchAction !== pan)
|
|
560
|
+
this.Element.style.touchAction = pan;
|
|
344
561
|
};
|
|
562
|
+
pageObserver;
|
|
345
563
|
AttachInput() {
|
|
346
564
|
const el = this.Element;
|
|
347
|
-
|
|
565
|
+
this.UpdateTouchAction();
|
|
566
|
+
window.addEventListener("resize", this.UpdateTouchAction);
|
|
567
|
+
this.pageObserver = new ResizeObserver(this.UpdateTouchAction);
|
|
568
|
+
this.pageObserver.observe(document.documentElement);
|
|
569
|
+
this.pageObserver.observe(document.body);
|
|
348
570
|
el.style.userSelect = "none";
|
|
349
571
|
for (const t of ["pointerdown", "pointermove", "pointerup", "pointercancel"])
|
|
350
572
|
el.addEventListener(t, this.onPointer);
|
|
573
|
+
el.addEventListener("pointerleave", this.onPointerLeave);
|
|
351
574
|
el.addEventListener("contextmenu", this.onContextMenu);
|
|
352
575
|
el.addEventListener("wheel", this.onWheel, { passive: false });
|
|
353
|
-
if (this.gestures === "Lock")
|
|
576
|
+
if (this.gestures === "Lock") {
|
|
354
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
|
+
}
|
|
587
|
+
else if (this.gestures === "Enabled")
|
|
588
|
+
el.addEventListener("touchmove", this.preventClaimedTouch, { passive: false });
|
|
355
589
|
}
|
|
590
|
+
/** The page's own overscroll-behavior before Lock set it to none (html, body), put back on detach. */
|
|
591
|
+
pageOverscroll;
|
|
356
592
|
DetachInput() {
|
|
357
593
|
const el = this.Element;
|
|
358
594
|
el.style.touchAction = "";
|
|
595
|
+
window.removeEventListener("resize", this.UpdateTouchAction);
|
|
596
|
+
this.pageObserver?.disconnect();
|
|
597
|
+
this.pageObserver = undefined;
|
|
359
598
|
el.style.userSelect = "";
|
|
360
599
|
for (const t of ["pointerdown", "pointermove", "pointerup", "pointercancel"])
|
|
361
600
|
el.removeEventListener(t, this.onPointer);
|
|
601
|
+
el.removeEventListener("pointerleave", this.onPointerLeave);
|
|
362
602
|
el.removeEventListener("contextmenu", this.onContextMenu);
|
|
363
603
|
el.removeEventListener("wheel", this.onWheel);
|
|
364
604
|
el.removeEventListener("touchmove", this.preventTouch);
|
|
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
|
+
}
|
|
613
|
+
this.claimedTouches.clear();
|
|
614
|
+
for (const id of [...this.longPressTimers.keys()])
|
|
615
|
+
this.DisarmLongPress(id);
|
|
616
|
+
this.longPressed.clear();
|
|
365
617
|
this.activeTouchIds.clear();
|
|
366
618
|
this.pointerDownArgs.clear();
|
|
367
619
|
this.previousTouchArgs.clear();
|
|
@@ -372,12 +624,23 @@ export class Canvas {
|
|
|
372
624
|
return;
|
|
373
625
|
const id = args.Id;
|
|
374
626
|
if (args.Type === "Pressed") {
|
|
627
|
+
this.ArmLongPress(id, args);
|
|
375
628
|
this.activeTouchIds.add(id);
|
|
376
629
|
args.NumberOfTouches = this.activeTouchIds.size;
|
|
377
630
|
args.StartingLocation = args.Location;
|
|
378
631
|
args.IsInContact = true;
|
|
379
632
|
this.pointerDownArgs.set(id, args);
|
|
380
633
|
this.previousTouchArgs.set(id, args);
|
|
634
|
+
// Gestures="Enabled", touch or pen: page scrolling was left to touch-action alone, and iOS Safari still takes an
|
|
635
|
+
// off-axis drag from the page axis (pointercancel mid-gesture, w3c/pointerevents#303). So the Down is processed
|
|
636
|
+
// now, like the wheel (queued gestures first, same "used" rule): when a control used it the touch is claimed and
|
|
637
|
+
// its touchmoves are default-prevented until it ends. It is not queued as well, so it is processed exactly once and
|
|
638
|
+
// the Panning / Up that follow keep their order. An unclaimed touch shares the page as before.
|
|
639
|
+
if (this.gestures === "Enabled" && args.Pointer && args.Pointer.DeviceType !== "Mouse") {
|
|
640
|
+
if (this.ProcessGestureNow(args, "Down"))
|
|
641
|
+
this.claimedTouches.add(id);
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
381
644
|
this.OnGestureEvent(args, "Down");
|
|
382
645
|
return;
|
|
383
646
|
}
|
|
@@ -386,6 +649,9 @@ export class Canvas {
|
|
|
386
649
|
const downArgs = this.pointerDownArgs.get(id);
|
|
387
650
|
args.StartingLocation = downArgs ? downArgs.StartingLocation : args.Location;
|
|
388
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);
|
|
389
655
|
if (args.Distance.Delta.X !== 0 || args.Distance.Delta.Y !== 0)
|
|
390
656
|
this.OnGestureEvent(args, "Panning");
|
|
391
657
|
this.previousTouchArgs.set(id, args);
|
|
@@ -393,7 +659,9 @@ export class Canvas {
|
|
|
393
659
|
}
|
|
394
660
|
if (args.Type === "Released" || args.Type === "Cancelled") {
|
|
395
661
|
args.IsInContact = args.NumberOfTouches > 1;
|
|
396
|
-
|
|
662
|
+
const longPressed = this.longPressed.delete(id);
|
|
663
|
+
this.DisarmLongPress(id);
|
|
664
|
+
if (!args.IsInContact && downArgs && args.Type === "Released" && !longPressed) {
|
|
397
665
|
const threshold = Canvas.TappedCancelMoveThresholdPoints * Math.max(0.1, this.RenderingScale);
|
|
398
666
|
if (Math.abs(args.Distance.Total.X) < threshold && Math.abs(args.Distance.Total.Y) < threshold)
|
|
399
667
|
this.OnGestureEvent(args, "Tapped");
|
|
@@ -404,6 +672,30 @@ export class Canvas {
|
|
|
404
672
|
this.activeTouchIds.delete(id);
|
|
405
673
|
}
|
|
406
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
|
+
}
|
|
407
699
|
OnGestureEvent(args, result) {
|
|
408
700
|
this.pendingGestures.push(SkiaGesturesParameters.Create(result, args));
|
|
409
701
|
this.Update();
|
|
@@ -418,8 +710,44 @@ export class Canvas {
|
|
|
418
710
|
for (const args of batch)
|
|
419
711
|
this.ProcessGestures(root, args);
|
|
420
712
|
}
|
|
713
|
+
/**
|
|
714
|
+
* The control that consumed the current gesture keeps it until it lets go (DrawnUi Canvas.HadInput). Without this a
|
|
715
|
+
* gesture is re-routed by the pointer position on every event, so anything the finger drags away from — a drag
|
|
716
|
+
* handle, a slider thumb, a button being held — stops receiving Panning the moment the pointer leaves its box.
|
|
717
|
+
* Single pointer, which is what the web gives us here; C# keeps a set for multi-touch.
|
|
718
|
+
*/
|
|
719
|
+
gestureOwner = null;
|
|
720
|
+
/** DrawnUi IsSavedGesture: the types replayed to the owner instead of being routed again. */
|
|
721
|
+
static IsSavedGesture(type) {
|
|
722
|
+
return type === "Panning" || type === "Wheel" || type === "Up";
|
|
723
|
+
}
|
|
421
724
|
/** Entry into the control tree, same shape as DrawnUi Canvas.ProcessGestures. */
|
|
422
725
|
ProcessGestures(root, args) {
|
|
423
|
-
|
|
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";
|
|
731
|
+
if (args.Type === "Down")
|
|
732
|
+
this.gestureOwner = null;
|
|
733
|
+
else if (this.gestureOwner && Canvas.IsSavedGesture(args.Type) && (!wheel || this.activeTouchIds.size > 0)) {
|
|
734
|
+
const owner = this.gestureOwner;
|
|
735
|
+
const alive = !!owner.Superview && owner.IsVisible && !owner.InputTransparent;
|
|
736
|
+
const consumed = alive ? owner.OnSkiaGestureEvent(args, info()) : null;
|
|
737
|
+
if (consumed) {
|
|
738
|
+
// it still wants the gesture: nobody else sees this one (C# skips the tree pass for a saved gesture)
|
|
739
|
+
if (!wheel)
|
|
740
|
+
this.gestureOwner = args.Type === "Up" ? null : consumed;
|
|
741
|
+
return consumed;
|
|
742
|
+
}
|
|
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;
|
|
747
|
+
}
|
|
748
|
+
const consumed = root.ProcessGestures(args, info());
|
|
749
|
+
if (!wheel)
|
|
750
|
+
this.gestureOwner = consumed && args.Type !== "Up" ? consumed : null;
|
|
751
|
+
return consumed;
|
|
424
752
|
}
|
|
425
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
|
}
|