drawnui-react 0.1.0-preview.1
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/LICENSE +21 -0
- package/PARITY.md +287 -0
- package/README.md +92 -0
- package/SKIPPED.md +107 -0
- package/dist/controls/AnimatedFramesRenderer.d.ts +58 -0
- package/dist/controls/AnimatedFramesRenderer.js +133 -0
- package/dist/controls/GridStructure.d.ts +79 -0
- package/dist/controls/GridStructure.js +403 -0
- package/dist/controls/RefreshIndicator.d.ts +35 -0
- package/dist/controls/RefreshIndicator.js +82 -0
- package/dist/controls/SkiaBackdrop.d.ts +25 -0
- package/dist/controls/SkiaBackdrop.js +80 -0
- package/dist/controls/SkiaButton.d.ts +58 -0
- package/dist/controls/SkiaButton.js +148 -0
- package/dist/controls/SkiaCarousel.d.ts +133 -0
- package/dist/controls/SkiaCarousel.js +603 -0
- package/dist/controls/SkiaCheckbox.d.ts +21 -0
- package/dist/controls/SkiaCheckbox.js +118 -0
- package/dist/controls/SkiaDecoratedGrid.d.ts +15 -0
- package/dist/controls/SkiaDecoratedGrid.js +46 -0
- package/dist/controls/SkiaDrawer.d.ts +46 -0
- package/dist/controls/SkiaDrawer.js +212 -0
- package/dist/controls/SkiaDynamicDrawnCell.d.ts +11 -0
- package/dist/controls/SkiaDynamicDrawnCell.js +17 -0
- package/dist/controls/SkiaEditor.d.ts +163 -0
- package/dist/controls/SkiaEditor.js +803 -0
- package/dist/controls/SkiaGif.d.ts +54 -0
- package/dist/controls/SkiaGif.js +206 -0
- package/dist/controls/SkiaHotspot.d.ts +13 -0
- package/dist/controls/SkiaHotspot.js +36 -0
- package/dist/controls/SkiaImage.d.ts +75 -0
- package/dist/controls/SkiaImage.js +222 -0
- package/dist/controls/SkiaImageTiles.d.ts +27 -0
- package/dist/controls/SkiaImageTiles.js +76 -0
- package/dist/controls/SkiaLabel.d.ts +160 -0
- package/dist/controls/SkiaLabel.js +580 -0
- package/dist/controls/SkiaLayout.d.ts +163 -0
- package/dist/controls/SkiaLayout.js +723 -0
- package/dist/controls/SkiaLottie.d.ts +75 -0
- package/dist/controls/SkiaLottie.js +290 -0
- package/dist/controls/SkiaProgress.d.ts +40 -0
- package/dist/controls/SkiaProgress.js +80 -0
- package/dist/controls/SkiaRadioButton.d.ts +32 -0
- package/dist/controls/SkiaRadioButton.js +157 -0
- package/dist/controls/SkiaRichLabel.d.ts +77 -0
- package/dist/controls/SkiaRichLabel.js +274 -0
- package/dist/controls/SkiaScroll.d.ts +231 -0
- package/dist/controls/SkiaScroll.js +958 -0
- package/dist/controls/SkiaScrollBar.d.ts +56 -0
- package/dist/controls/SkiaScrollBar.js +158 -0
- package/dist/controls/SkiaShaderCarousel.d.ts +67 -0
- package/dist/controls/SkiaShaderCarousel.js +246 -0
- package/dist/controls/SkiaShape.d.ts +63 -0
- package/dist/controls/SkiaShape.js +317 -0
- package/dist/controls/SkiaSlider.d.ts +74 -0
- package/dist/controls/SkiaSlider.js +243 -0
- package/dist/controls/SkiaSprite.d.ts +99 -0
- package/dist/controls/SkiaSprite.js +324 -0
- package/dist/controls/SkiaSpriteSet.d.ts +27 -0
- package/dist/controls/SkiaSpriteSet.js +75 -0
- package/dist/controls/SkiaSvg.d.ts +43 -0
- package/dist/controls/SkiaSvg.js +181 -0
- package/dist/controls/SkiaSwitch.d.ts +26 -0
- package/dist/controls/SkiaSwitch.js +147 -0
- package/dist/controls/SkiaToggle.d.ts +50 -0
- package/dist/controls/SkiaToggle.js +71 -0
- package/dist/controls/SnappingLayout.d.ts +78 -0
- package/dist/controls/SnappingLayout.js +217 -0
- package/dist/controls/TextSpan.d.ts +70 -0
- package/dist/controls/TextSpan.js +76 -0
- package/dist/core/Accessibility.d.ts +104 -0
- package/dist/core/Accessibility.js +150 -0
- package/dist/core/Animators.d.ts +82 -0
- package/dist/core/Animators.js +247 -0
- package/dist/core/Canvas.d.ts +89 -0
- package/dist/core/Canvas.js +328 -0
- package/dist/core/ControlStyle.d.ts +6 -0
- package/dist/core/ControlStyle.js +11 -0
- package/dist/core/Easing.d.ts +13 -0
- package/dist/core/Easing.js +15 -0
- package/dist/core/Gestures.d.ts +74 -0
- package/dist/core/Gestures.js +104 -0
- package/dist/core/ImageEffects.d.ts +29 -0
- package/dist/core/ImageEffects.js +55 -0
- package/dist/core/KeyboardManager.d.ts +25 -0
- package/dist/core/KeyboardManager.js +54 -0
- package/dist/core/ScrollAnimators.d.ts +90 -0
- package/dist/core/ScrollAnimators.js +244 -0
- package/dist/core/SkiaControl.d.ts +401 -0
- package/dist/core/SkiaControl.js +1239 -0
- package/dist/core/SkiaEffect.d.ts +216 -0
- package/dist/core/SkiaEffect.js +571 -0
- package/dist/core/SkiaImageManager.d.ts +43 -0
- package/dist/core/SkiaImageManager.js +122 -0
- package/dist/core/Super.d.ts +61 -0
- package/dist/core/Super.js +178 -0
- package/dist/core/TextInputProxy.d.ts +31 -0
- package/dist/core/TextInputProxy.js +144 -0
- package/dist/core/Types.d.ts +177 -0
- package/dist/core/Types.js +122 -0
- package/dist/core/ViewsAdapter.d.ts +41 -0
- package/dist/core/ViewsAdapter.js +113 -0
- package/dist/index.d.ts +48 -0
- package/dist/index.js +48 -0
- package/dist/react/SkiaShell.d.ts +144 -0
- package/dist/react/SkiaShell.js +490 -0
- package/dist/react/index.d.ts +110 -0
- package/dist/react/index.js +158 -0
- package/dist/react/reconciler.d.ts +14 -0
- package/dist/react/reconciler.js +160 -0
- package/package.json +60 -0
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
export class KeyboardManager {
|
|
2
|
+
static attached = false;
|
|
3
|
+
static pressed = new Set();
|
|
4
|
+
static keyDown = new Set();
|
|
5
|
+
static keyUp = new Set();
|
|
6
|
+
static keyChar = new Set();
|
|
7
|
+
static get IsShiftPressed() { return KeyboardManager.pressed.has("ShiftLeft") || KeyboardManager.pressed.has("ShiftRight"); }
|
|
8
|
+
static get IsControlPressed() { return KeyboardManager.pressed.has("ControlLeft") || KeyboardManager.pressed.has("ControlRight") || KeyboardManager.pressed.has("MetaLeft") || KeyboardManager.pressed.has("MetaRight"); }
|
|
9
|
+
static get IsAltPressed() { return KeyboardManager.pressed.has("AltLeft") || KeyboardManager.pressed.has("AltRight"); }
|
|
10
|
+
/** C# AttachToKeyboardAsync / JS attachGlobalKeyboard: idempotent. */
|
|
11
|
+
static AttachToKeyboard() {
|
|
12
|
+
if (KeyboardManager.attached || typeof window === "undefined")
|
|
13
|
+
return;
|
|
14
|
+
KeyboardManager.attached = true;
|
|
15
|
+
// inside a cross-frame iframe the embedding page keeps DOM focus: a pointerdown pulls it to this window
|
|
16
|
+
window.addEventListener("pointerdown", () => window.focus(), true);
|
|
17
|
+
window.addEventListener("keydown", (e) => {
|
|
18
|
+
const code = e.code || "";
|
|
19
|
+
KeyboardManager.pressed.add(code);
|
|
20
|
+
for (const h of [...KeyboardManager.keyDown])
|
|
21
|
+
h(code, e);
|
|
22
|
+
const proxied = e.target?.dataset?.drawnuiTextInput === "1"; // TextInputProxy: characters come through its input events
|
|
23
|
+
if (!proxied && e.key && e.key.length === 1 && !e.ctrlKey && !e.altKey && !e.metaKey)
|
|
24
|
+
for (const h of [...KeyboardManager.keyChar])
|
|
25
|
+
h(e.key, e);
|
|
26
|
+
}, true);
|
|
27
|
+
window.addEventListener("keyup", (e) => {
|
|
28
|
+
const code = e.code || "";
|
|
29
|
+
KeyboardManager.pressed.delete(code);
|
|
30
|
+
for (const h of [...KeyboardManager.keyUp])
|
|
31
|
+
h(code, e);
|
|
32
|
+
}, true);
|
|
33
|
+
window.addEventListener("blur", () => KeyboardManager.pressed.clear());
|
|
34
|
+
}
|
|
35
|
+
static Subscribe(down, char, up) {
|
|
36
|
+
KeyboardManager.AttachToKeyboard();
|
|
37
|
+
KeyboardManager.keyDown.add(down);
|
|
38
|
+
KeyboardManager.keyChar.add(char);
|
|
39
|
+
if (up)
|
|
40
|
+
KeyboardManager.keyUp.add(up);
|
|
41
|
+
}
|
|
42
|
+
static Unsubscribe(down, char, up) {
|
|
43
|
+
KeyboardManager.keyDown.delete(down);
|
|
44
|
+
KeyboardManager.keyChar.delete(char);
|
|
45
|
+
if (up)
|
|
46
|
+
KeyboardManager.keyUp.delete(up);
|
|
47
|
+
}
|
|
48
|
+
/** JS blurExternalTextInput: a page text input outside the canvas would keep receiving the keys. */
|
|
49
|
+
static BlurExternalTextInput() {
|
|
50
|
+
const a = typeof document !== "undefined" ? document.activeElement : null;
|
|
51
|
+
if (a && a.dataset?.drawnuiTextInput !== "1" && (a.tagName === "INPUT" || a.tagName === "TEXTAREA" || a.isContentEditable) && !a.closest("canvas"))
|
|
52
|
+
a.blur();
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { SkiaValueAnimator } from "./Animators";
|
|
2
|
+
import type { SkiaControl } from "./SkiaControl";
|
|
3
|
+
import { SKRect } from "./Types";
|
|
4
|
+
/** UIKit-style exponential deceleration: value(t) = v0 * (rate^(1000 t) - 1) / k, k = 1000 ln(rate). */
|
|
5
|
+
export declare class DecelerationTimingParameters {
|
|
6
|
+
InitialValue: number;
|
|
7
|
+
InitialVelocity: number;
|
|
8
|
+
readonly DecelerationRate: number;
|
|
9
|
+
readonly DecelerationK: number;
|
|
10
|
+
Threshold: number;
|
|
11
|
+
constructor(initialValue: number, initialVelocity: number, decelerationRate: number, threshold: number);
|
|
12
|
+
/** Initial velocity that lands on targetValue after durationSecs (DrawnUi second ctor). */
|
|
13
|
+
static ToDestination(currentValue: number, targetValue: number, durationSecs: number, decelerationRate: number, threshold?: number): DecelerationTimingParameters;
|
|
14
|
+
get Destination(): number;
|
|
15
|
+
get DurationSecs(): number;
|
|
16
|
+
ValueAt(secs: number): number;
|
|
17
|
+
VelocityAt(secs: number): number;
|
|
18
|
+
/** Seconds until the curve reaches value (used to cut a fling at the content edge). */
|
|
19
|
+
DurationToValue(value: number): number;
|
|
20
|
+
}
|
|
21
|
+
/** DrawnUi ScrollFlingAnimator: drives an offset along a deceleration curve; stops when the change rate stalls. */
|
|
22
|
+
export declare class ScrollFlingAnimator extends SkiaValueAnimator {
|
|
23
|
+
SelfFinished: boolean;
|
|
24
|
+
ValueThreshold: number;
|
|
25
|
+
Parameters?: DecelerationTimingParameters;
|
|
26
|
+
CurrentVelocity: number;
|
|
27
|
+
private lastValue;
|
|
28
|
+
private lastUpdateTime;
|
|
29
|
+
private belowThresholdFrames;
|
|
30
|
+
private static readonly FramesBelowThresholdToStop;
|
|
31
|
+
constructor(parent: SkiaControl);
|
|
32
|
+
InitializeWithVelocity(position: number, velocity: number, deceleration?: number, valueThreshold?: number): void;
|
|
33
|
+
/** Reaches target exactly at timeSecs; the curve is stopped there (its asymptote lies beyond the target). */
|
|
34
|
+
InitializeWithDestination(position: number, target: number, timeSecs: number, deceleration?: number, valueThreshold?: number): void;
|
|
35
|
+
Start(delayMs?: number): void;
|
|
36
|
+
/** Speed here is SECONDS (DurationSecs), unlike SkiaValueAnimator's ms — same as the C# class. */
|
|
37
|
+
protected UpdateValue(deltaT: number, deltaFromStart: number): boolean;
|
|
38
|
+
}
|
|
39
|
+
/** DrawnUi Spring. */
|
|
40
|
+
export declare class Spring {
|
|
41
|
+
Mass: number;
|
|
42
|
+
Stiffness: number;
|
|
43
|
+
DampingRatio: number;
|
|
44
|
+
constructor(Mass: number, Stiffness: number, DampingRatio: number);
|
|
45
|
+
static get Damped(): Spring;
|
|
46
|
+
static get Default(): Spring;
|
|
47
|
+
}
|
|
48
|
+
/** DrawnUi SpringTimingParameters (critically damped or underdamped). Displacement decays to 0. */
|
|
49
|
+
export declare class SpringTimingParameters {
|
|
50
|
+
readonly Spring: Spring;
|
|
51
|
+
readonly Displacement: number;
|
|
52
|
+
readonly InitialVelocity: number;
|
|
53
|
+
readonly Threshold: number;
|
|
54
|
+
private readonly beta;
|
|
55
|
+
private readonly wd;
|
|
56
|
+
private readonly c1;
|
|
57
|
+
private readonly c2;
|
|
58
|
+
readonly DurationSecs: number;
|
|
59
|
+
constructor(Spring: Spring, Displacement: number, InitialVelocity: number, Threshold: number);
|
|
60
|
+
ValueAt(t: number): number;
|
|
61
|
+
}
|
|
62
|
+
/** DrawnUi SpringWithVelocityAnimator: returns an offset to restOffset from a displacement with initial velocity. */
|
|
63
|
+
export declare class SpringWithVelocityAnimator extends SkiaValueAnimator {
|
|
64
|
+
private origin;
|
|
65
|
+
Parameters?: SpringTimingParameters;
|
|
66
|
+
constructor(parent: SkiaControl);
|
|
67
|
+
Initialize(restOffset: number, position: number, velocity: number, spring: Spring, thresholdStop?: number): void;
|
|
68
|
+
protected UpdateValue(_deltaT: number, deltaFromStart: number): boolean;
|
|
69
|
+
}
|
|
70
|
+
/** DrawnUi VelocityAccumulator: weighted average of the last samples inside a 150 ms window. */
|
|
71
|
+
export declare class VelocityAccumulator {
|
|
72
|
+
private readonly samples;
|
|
73
|
+
private static readonly MaxSampleSize;
|
|
74
|
+
private static readonly ConsiderationTimeframeMs;
|
|
75
|
+
Clear(): void;
|
|
76
|
+
CaptureVelocity(x: number, y: number, arrivedTimeNanos?: number): void;
|
|
77
|
+
CalculateFinalVelocity(clampAbsolute?: number): {
|
|
78
|
+
X: number;
|
|
79
|
+
Y: number;
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
/** DrawnUi RubberBandUtils. */
|
|
83
|
+
export declare const RubberBandUtils: {
|
|
84
|
+
/** Clamps coord into limits with an iOS-style rubber band beyond them; dim = the viewport dimension. */
|
|
85
|
+
RubberBandClamp(coord: number, dim: number, start: number, end: number, coeff?: number, onEmpty?: number): number;
|
|
86
|
+
ClampOnTrack(x: number, y: number, track: SKRect, coeff: number, dimX: number, dimY: number): {
|
|
87
|
+
X: number;
|
|
88
|
+
Y: number;
|
|
89
|
+
};
|
|
90
|
+
};
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
// Mirrors DrawnUi scrolling physics: DecelerationTimingParameters + ScrollFlingAnimator (fling),
|
|
2
|
+
// Spring + SpringTimingParameters + SpringWithVelocityAnimator (bounce), VelocityAccumulator, RubberBandUtils.
|
|
3
|
+
import { SkiaValueAnimator } from "./Animators";
|
|
4
|
+
/** UIKit-style exponential deceleration: value(t) = v0 * (rate^(1000 t) - 1) / k, k = 1000 ln(rate). */
|
|
5
|
+
export class DecelerationTimingParameters {
|
|
6
|
+
InitialValue;
|
|
7
|
+
InitialVelocity;
|
|
8
|
+
DecelerationRate;
|
|
9
|
+
DecelerationK;
|
|
10
|
+
Threshold;
|
|
11
|
+
constructor(initialValue, initialVelocity, decelerationRate, threshold) {
|
|
12
|
+
if (decelerationRate <= 0 || decelerationRate >= 1)
|
|
13
|
+
throw new RangeError("Deceleration rate must be in (0, 1)");
|
|
14
|
+
this.InitialValue = initialValue;
|
|
15
|
+
this.InitialVelocity = initialVelocity;
|
|
16
|
+
this.Threshold = threshold;
|
|
17
|
+
this.DecelerationRate = decelerationRate;
|
|
18
|
+
this.DecelerationK = 1000 * Math.log(decelerationRate);
|
|
19
|
+
}
|
|
20
|
+
/** Initial velocity that lands on targetValue after durationSecs (DrawnUi second ctor). */
|
|
21
|
+
static ToDestination(currentValue, targetValue, durationSecs, decelerationRate, threshold = 0.1) {
|
|
22
|
+
if (durationSecs <= 0)
|
|
23
|
+
throw new RangeError("Duration must be > 0");
|
|
24
|
+
const p = new DecelerationTimingParameters(currentValue, 0, decelerationRate, threshold);
|
|
25
|
+
const distance = targetValue - currentValue;
|
|
26
|
+
if (Math.abs(distance) < 1e-5)
|
|
27
|
+
return p;
|
|
28
|
+
const denominator = Math.pow(decelerationRate, 1000 * durationSecs) - 1;
|
|
29
|
+
p.InitialVelocity = Math.abs(denominator) < 1e-5 ? distance / durationSecs : (distance * p.DecelerationK) / denominator;
|
|
30
|
+
return p;
|
|
31
|
+
}
|
|
32
|
+
get Destination() { return this.InitialVelocity === 0 ? this.InitialValue : this.InitialValue - this.InitialVelocity / this.DecelerationK; }
|
|
33
|
+
get DurationSecs() {
|
|
34
|
+
if (this.InitialVelocity === 0)
|
|
35
|
+
return 0;
|
|
36
|
+
const divisor = (-this.DecelerationK * this.Threshold) / Math.abs(this.InitialVelocity);
|
|
37
|
+
return divisor <= 0 ? 0 : Math.log(divisor) / this.DecelerationK;
|
|
38
|
+
}
|
|
39
|
+
ValueAt(secs) {
|
|
40
|
+
if (this.DecelerationK === 0)
|
|
41
|
+
return this.InitialValue;
|
|
42
|
+
return this.InitialValue + this.InitialVelocity * ((Math.pow(this.DecelerationRate, 1000 * secs) - 1) / this.DecelerationK);
|
|
43
|
+
}
|
|
44
|
+
VelocityAt(secs) { return this.InitialVelocity * Math.pow(this.DecelerationRate, 1000 * secs); }
|
|
45
|
+
/** Seconds until the curve reaches value (used to cut a fling at the content edge). */
|
|
46
|
+
DurationToValue(value) {
|
|
47
|
+
if (this.DecelerationK === 0 || this.InitialVelocity === 0)
|
|
48
|
+
return 0;
|
|
49
|
+
const distance = Math.abs(value - this.InitialValue);
|
|
50
|
+
return distance === 0 ? 0 : Math.log(1 + (this.DecelerationK * distance) / Math.abs(this.InitialVelocity)) / this.DecelerationK;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/** DrawnUi ScrollFlingAnimator: drives an offset along a deceleration curve; stops when the change rate stalls. */
|
|
54
|
+
export class ScrollFlingAnimator extends SkiaValueAnimator {
|
|
55
|
+
SelfFinished = false;
|
|
56
|
+
ValueThreshold = 0.1;
|
|
57
|
+
Parameters;
|
|
58
|
+
CurrentVelocity = 0;
|
|
59
|
+
lastValue = 0;
|
|
60
|
+
lastUpdateTime = 0;
|
|
61
|
+
belowThresholdFrames = 0;
|
|
62
|
+
static FramesBelowThresholdToStop = 3;
|
|
63
|
+
constructor(parent) { super(parent); }
|
|
64
|
+
InitializeWithVelocity(position, velocity, deceleration = 0.998, valueThreshold = 1.85) {
|
|
65
|
+
this.Parameters = new DecelerationTimingParameters(position, velocity, deceleration, 0.001);
|
|
66
|
+
this.Speed = this.Parameters.DurationSecs;
|
|
67
|
+
this.ValueThreshold = valueThreshold;
|
|
68
|
+
this.lastValue = position;
|
|
69
|
+
this.belowThresholdFrames = 0;
|
|
70
|
+
}
|
|
71
|
+
/** Reaches target exactly at timeSecs; the curve is stopped there (its asymptote lies beyond the target). */
|
|
72
|
+
InitializeWithDestination(position, target, timeSecs, deceleration = 0.998, valueThreshold = 0.1) {
|
|
73
|
+
this.Parameters = DecelerationTimingParameters.ToDestination(position, target, timeSecs, deceleration, 0.001);
|
|
74
|
+
this.Speed = timeSecs;
|
|
75
|
+
this.ValueThreshold = valueThreshold;
|
|
76
|
+
this.lastValue = position;
|
|
77
|
+
this.belowThresholdFrames = 0;
|
|
78
|
+
}
|
|
79
|
+
Start(delayMs = 0) {
|
|
80
|
+
this.SelfFinished = false;
|
|
81
|
+
this.belowThresholdFrames = 0;
|
|
82
|
+
this.lastUpdateTime = 0;
|
|
83
|
+
super.Start(delayMs);
|
|
84
|
+
}
|
|
85
|
+
/** Speed here is SECONDS (DurationSecs), unlike SkiaValueAnimator's ms — same as the C# class. */
|
|
86
|
+
UpdateValue(deltaT, deltaFromStart) {
|
|
87
|
+
const p = this.Parameters;
|
|
88
|
+
if (!p)
|
|
89
|
+
return true;
|
|
90
|
+
const secs = deltaFromStart / 1e9;
|
|
91
|
+
if (secs > this.Speed) {
|
|
92
|
+
// Land exactly where the planned duration ends (edge or destination), not a frame past it.
|
|
93
|
+
this.mValue = p.ValueAt(this.Speed);
|
|
94
|
+
this.CurrentVelocity = p.VelocityAt(this.Speed);
|
|
95
|
+
this.SelfFinished = true;
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
this.mValue = p.ValueAt(secs);
|
|
99
|
+
this.CurrentVelocity = p.VelocityAt(secs);
|
|
100
|
+
if (this.lastUpdateTime > 0) {
|
|
101
|
+
const dt = deltaT / 1e9;
|
|
102
|
+
const changeRate = dt > 0 ? Math.abs(this.mValue - this.lastValue) / dt : 0;
|
|
103
|
+
if (changeRate < this.ValueThreshold) {
|
|
104
|
+
if (++this.belowThresholdFrames >= ScrollFlingAnimator.FramesBelowThresholdToStop) {
|
|
105
|
+
this.SelfFinished = true;
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
else
|
|
110
|
+
this.belowThresholdFrames = 0;
|
|
111
|
+
}
|
|
112
|
+
this.lastValue = this.mValue;
|
|
113
|
+
this.lastUpdateTime = deltaFromStart;
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
/** DrawnUi Spring. */
|
|
118
|
+
export class Spring {
|
|
119
|
+
Mass;
|
|
120
|
+
Stiffness;
|
|
121
|
+
DampingRatio;
|
|
122
|
+
constructor(Mass, Stiffness, DampingRatio) {
|
|
123
|
+
this.Mass = Mass;
|
|
124
|
+
this.Stiffness = Stiffness;
|
|
125
|
+
this.DampingRatio = DampingRatio;
|
|
126
|
+
}
|
|
127
|
+
static get Damped() { return new Spring(1, 200, 1); }
|
|
128
|
+
static get Default() { return new Spring(1, 200, 0.5); }
|
|
129
|
+
}
|
|
130
|
+
/** DrawnUi SpringTimingParameters (critically damped or underdamped). Displacement decays to 0. */
|
|
131
|
+
export class SpringTimingParameters {
|
|
132
|
+
Spring;
|
|
133
|
+
Displacement;
|
|
134
|
+
InitialVelocity;
|
|
135
|
+
Threshold;
|
|
136
|
+
beta;
|
|
137
|
+
wd;
|
|
138
|
+
c1;
|
|
139
|
+
c2;
|
|
140
|
+
DurationSecs;
|
|
141
|
+
constructor(Spring, Displacement, InitialVelocity, Threshold) {
|
|
142
|
+
this.Spring = Spring;
|
|
143
|
+
this.Displacement = Displacement;
|
|
144
|
+
this.InitialVelocity = InitialVelocity;
|
|
145
|
+
this.Threshold = Threshold;
|
|
146
|
+
if (Spring.DampingRatio <= 0 || Spring.DampingRatio > 1)
|
|
147
|
+
throw new RangeError("dampingRatio should be in (0, 1]");
|
|
148
|
+
const w0 = Math.sqrt(Spring.Stiffness / Spring.Mass);
|
|
149
|
+
this.beta = Spring.DampingRatio * w0 * 2;
|
|
150
|
+
this.c1 = Displacement;
|
|
151
|
+
if (Spring.DampingRatio >= 1) {
|
|
152
|
+
this.wd = 0;
|
|
153
|
+
this.c2 = InitialVelocity + this.beta * Displacement;
|
|
154
|
+
if (Displacement === 0 && InitialVelocity === 0)
|
|
155
|
+
this.DurationSecs = 0;
|
|
156
|
+
else {
|
|
157
|
+
const t1 = (1 / this.beta) * Math.log((2 * Math.abs(this.c1)) / Threshold);
|
|
158
|
+
const t2 = (2 / this.beta) * Math.log((4 * Math.abs(this.c2)) / (Math.E * this.beta * Threshold));
|
|
159
|
+
this.DurationSecs = Math.max(t1, t2);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
this.wd = w0 * Math.sqrt(1 - Spring.DampingRatio * Spring.DampingRatio);
|
|
164
|
+
this.c2 = (InitialVelocity + this.beta * Displacement) / this.wd;
|
|
165
|
+
this.DurationSecs = Displacement === 0 && InitialVelocity === 0 ? 0 : Math.log((Math.abs(this.c1) + Math.abs(this.c2)) / Threshold) / this.beta;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
ValueAt(t) {
|
|
169
|
+
if (this.wd === 0)
|
|
170
|
+
return Math.exp(-this.beta * t) * (this.c1 + this.c2 * t);
|
|
171
|
+
return Math.exp(-this.beta * t) * (this.c1 * Math.cos(this.wd * t) + this.c2 * Math.sin(this.wd * t));
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
/** DrawnUi SpringWithVelocityAnimator: returns an offset to restOffset from a displacement with initial velocity. */
|
|
175
|
+
export class SpringWithVelocityAnimator extends SkiaValueAnimator {
|
|
176
|
+
origin = 0;
|
|
177
|
+
Parameters;
|
|
178
|
+
constructor(parent) { super(parent); }
|
|
179
|
+
Initialize(restOffset, position, velocity, spring, thresholdStop = 0.5) {
|
|
180
|
+
this.origin = restOffset;
|
|
181
|
+
this.Parameters = new SpringTimingParameters(spring, position, velocity, thresholdStop);
|
|
182
|
+
}
|
|
183
|
+
UpdateValue(_deltaT, deltaFromStart) {
|
|
184
|
+
const p = this.Parameters;
|
|
185
|
+
if (!p)
|
|
186
|
+
return true;
|
|
187
|
+
const secs = deltaFromStart / 1e9;
|
|
188
|
+
if (secs > p.DurationSecs) {
|
|
189
|
+
this.mValue = this.origin;
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
this.mValue = this.origin + p.ValueAt(secs);
|
|
193
|
+
return false;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
/** DrawnUi VelocityAccumulator: weighted average of the last samples inside a 150 ms window. */
|
|
197
|
+
export class VelocityAccumulator {
|
|
198
|
+
samples = [];
|
|
199
|
+
static MaxSampleSize = 5;
|
|
200
|
+
static ConsiderationTimeframeMs = 150;
|
|
201
|
+
Clear() { this.samples.length = 0; }
|
|
202
|
+
CaptureVelocity(x, y, arrivedTimeNanos = 0) {
|
|
203
|
+
const time = arrivedTimeNanos > 0 ? arrivedTimeNanos / 1e6 : performance.now();
|
|
204
|
+
if (this.samples.length === VelocityAccumulator.MaxSampleSize)
|
|
205
|
+
this.samples.shift();
|
|
206
|
+
this.samples.push({ X: x, Y: y, time });
|
|
207
|
+
}
|
|
208
|
+
CalculateFinalVelocity(clampAbsolute = 0) {
|
|
209
|
+
const now = performance.now();
|
|
210
|
+
const relevant = this.samples.filter((s) => now - s.time <= VelocityAccumulator.ConsiderationTimeframeMs);
|
|
211
|
+
if (relevant.length === 0)
|
|
212
|
+
return { X: 0, Y: 0 };
|
|
213
|
+
let sx = 0, sy = 0, weights = 0;
|
|
214
|
+
relevant.forEach((s, i) => { sx += s.X * (i + 1); sy += s.Y * (i + 1); weights += i + 1; });
|
|
215
|
+
let x = sx / weights, y = sy / weights;
|
|
216
|
+
if (clampAbsolute !== 0) {
|
|
217
|
+
x = Math.max(-clampAbsolute, Math.min(clampAbsolute, x));
|
|
218
|
+
y = Math.max(-clampAbsolute, Math.min(clampAbsolute, y));
|
|
219
|
+
}
|
|
220
|
+
return { X: x, Y: y };
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
/** DrawnUi RubberBandUtils. */
|
|
224
|
+
export const RubberBandUtils = {
|
|
225
|
+
/** Clamps coord into limits with an iOS-style rubber band beyond them; dim = the viewport dimension. */
|
|
226
|
+
RubberBandClamp(coord, dim, start, end, coeff = 0.275, onEmpty = 40) {
|
|
227
|
+
if (start > end)
|
|
228
|
+
return coord;
|
|
229
|
+
const clamped = Math.max(start, Math.min(end, coord));
|
|
230
|
+
const overscroll = coord < start ? coord - start : coord > end ? coord - end : 0;
|
|
231
|
+
if (overscroll === 0)
|
|
232
|
+
return clamped;
|
|
233
|
+
if (dim === 0)
|
|
234
|
+
dim = onEmpty;
|
|
235
|
+
const rubber = (1 - 1 / ((Math.abs(overscroll) * coeff) / dim + 1)) * dim;
|
|
236
|
+
return clamped + Math.sign(overscroll) * rubber;
|
|
237
|
+
},
|
|
238
|
+
ClampOnTrack(x, y, track, coeff, dimX, dimY) {
|
|
239
|
+
return {
|
|
240
|
+
X: RubberBandUtils.RubberBandClamp(x, dimX, track.Left, track.Right, coeff),
|
|
241
|
+
Y: RubberBandUtils.RubberBandClamp(y, dimY, track.Top, track.Bottom, coeff),
|
|
242
|
+
};
|
|
243
|
+
},
|
|
244
|
+
};
|