@rafaelaugusto/jolt 0.1.0-beta.14 → 0.1.0-beta.16
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 +11 -4
- package/dist/drag.js +1 -1
- package/dist/drag.js.map +1 -1
- package/dist/driver/android.d.ts.map +1 -1
- package/dist/driver/android.js +2 -0
- package/dist/driver/android.js.map +1 -1
- package/dist/driver/ios-scroll-delegate.d.ts +4 -0
- package/dist/driver/ios-scroll-delegate.d.ts.map +1 -0
- package/dist/driver/ios-scroll-delegate.js +26 -0
- package/dist/driver/ios-scroll-delegate.js.map +1 -0
- package/dist/driver/ios.d.ts.map +1 -1
- package/dist/driver/ios.js +2 -0
- package/dist/driver/ios.js.map +1 -1
- package/dist/gesture-native.d.ts +27 -0
- package/dist/gesture-native.d.ts.map +1 -0
- package/dist/gesture-native.js +55 -0
- package/dist/gesture-native.js.map +1 -0
- package/dist/gestures/index.d.ts +65 -0
- package/dist/gestures/index.d.ts.map +1 -0
- package/dist/gestures/index.js +251 -0
- package/dist/gestures/index.js.map +1 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/live-spring-driver.d.ts +52 -0
- package/dist/live-spring-driver.d.ts.map +1 -0
- package/dist/live-spring-driver.js +104 -0
- package/dist/live-spring-driver.js.map +1 -0
- package/dist/mixer.d.ts +6 -0
- package/dist/mixer.d.ts.map +1 -1
- package/dist/mixer.js +41 -1
- package/dist/mixer.js.map +1 -1
- package/dist/motion-scope.d.ts +2 -0
- package/dist/motion-scope.d.ts.map +1 -1
- package/dist/motion-scope.js +6 -0
- package/dist/motion-scope.js.map +1 -1
- package/dist/motion-value.d.ts +34 -2
- package/dist/motion-value.d.ts.map +1 -1
- package/dist/motion-value.js +277 -10
- package/dist/motion-value.js.map +1 -1
- package/dist/native/index.d.ts +1 -0
- package/dist/native/index.d.ts.map +1 -1
- package/dist/native/index.js +1 -0
- package/dist/native/index.js.map +1 -1
- package/dist/scene-camera.d.ts +5 -0
- package/dist/scene-camera.d.ts.map +1 -0
- package/dist/scene-camera.js +23 -0
- package/dist/scene-camera.js.map +1 -0
- package/dist/size-layout.d.ts +19 -0
- package/dist/size-layout.d.ts.map +1 -0
- package/dist/size-layout.js +216 -0
- package/dist/size-layout.js.map +1 -0
- package/dist/size.d.ts +19 -0
- package/dist/size.d.ts.map +1 -0
- package/dist/size.js +251 -0
- package/dist/size.js.map +1 -0
- package/dist/value-observers.d.ts +3 -1
- package/dist/value-observers.d.ts.map +1 -1
- package/dist/value-observers.js +10 -2
- package/dist/value-observers.js.map +1 -1
- package/package.json +5 -1
- package/platforms/android/include.gradle +9 -0
- package/platforms/android/java/org/rafaelaugusto/jolt/GestureSession.java +193 -0
- package/platforms/android/java/org/rafaelaugusto/jolt/GestureSurface.java +53 -0
- package/platforms/android/java/org/rafaelaugusto/jolt/LiveSpring.java +270 -0
- package/platforms/android/java/org/rafaelaugusto/jolt/SpringState.java +39 -0
- package/platforms/ios/src/JoltGestureSession.h +17 -0
- package/platforms/ios/src/JoltGestureSession.m +217 -0
- package/platforms/ios/src/JoltLiveSpring.h +28 -0
- package/platforms/ios/src/JoltLiveSpring.m +236 -0
- package/platforms/ios/src/JoltSpringState.h +35 -0
- package/platforms/ios/src/module.modulemap +2 -0
- package/src/android-plugin.d.ts +59 -0
- package/src/drag.ts +1 -1
- package/src/driver/android.ts +2 -0
- package/src/driver/ios-scroll-delegate.ts +23 -0
- package/src/driver/ios.ts +2 -0
- package/src/gesture-native.ts +124 -0
- package/src/gestures/index.ts +350 -0
- package/src/index.ts +5 -1
- package/src/ios-plugin.d.ts +61 -0
- package/src/live-spring-driver.ts +181 -0
- package/src/mixer.ts +43 -1
- package/src/motion-scope.ts +14 -0
- package/src/motion-value.ts +318 -11
- package/src/native/index.ts +1 -0
- package/src/scene-camera.ts +30 -0
- package/src/size-layout.ts +237 -0
- package/src/size.ts +302 -0
- package/src/value-observers.ts +12 -2
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
#import "JoltLiveSpring.h"
|
|
2
|
+
#import "JoltSpringState.h"
|
|
3
|
+
#import <QuartzCore/QuartzCore.h>
|
|
4
|
+
|
|
5
|
+
static id decode(NSString *json) {
|
|
6
|
+
return [NSJSONSerialization JSONObjectWithData:[json dataUsingEncoding:NSUTF8StringEncoding] options:0 error:nil];
|
|
7
|
+
}
|
|
8
|
+
static double mapped(double value, NSArray *mappings) {
|
|
9
|
+
for (NSDictionary *mapping in mappings) {
|
|
10
|
+
NSArray *input = mapping[@"input"], *output = mapping[@"output"];
|
|
11
|
+
BOOL clamp = [mapping[@"clamp"] boolValue];
|
|
12
|
+
if (clamp && value <= [input.firstObject doubleValue]) { value = [output.firstObject doubleValue]; continue; }
|
|
13
|
+
if (clamp && value >= [input.lastObject doubleValue]) { value = [output.lastObject doubleValue]; continue; }
|
|
14
|
+
NSUInteger index = 0;
|
|
15
|
+
while (index < input.count - 2 && value > [input[index + 1] doubleValue]) index++;
|
|
16
|
+
double span = [input[index + 1] doubleValue] - [input[index] doubleValue];
|
|
17
|
+
double fraction = span > 0 ? (value - [input[index] doubleValue]) / span : value < [input[index] doubleValue] ? 0 : 1;
|
|
18
|
+
value = [output[index] doubleValue] + ([output[index + 1] doubleValue] - [output[index] doubleValue]) * fraction;
|
|
19
|
+
}
|
|
20
|
+
return value;
|
|
21
|
+
}
|
|
22
|
+
static BOOL inputRunning(NSDictionary *input, double anchor, double now) {
|
|
23
|
+
double repeat = [input[@"repeat"] doubleValue];
|
|
24
|
+
return [input[@"running"] boolValue] && (repeat < 0 || now - anchor + [input[@"elapsed"] doubleValue] <
|
|
25
|
+
[input[@"delay"] doubleValue] + [input[@"duration"] doubleValue] * (repeat + 1));
|
|
26
|
+
}
|
|
27
|
+
static double inputValue(NSDictionary *input, double anchor, double now) {
|
|
28
|
+
NSArray *values = input[@"values"], *times = input[@"times"];
|
|
29
|
+
if (![input[@"running"] boolValue] || values.count < 2) return [input[@"value"] doubleValue];
|
|
30
|
+
if (!inputRunning(input, anchor, now)) return [input[@"to"] doubleValue];
|
|
31
|
+
double time = MAX(0, now - anchor + [input[@"elapsed"] doubleValue] - [input[@"delay"] doubleValue]);
|
|
32
|
+
double duration = MAX(1, [input[@"duration"] doubleValue]);
|
|
33
|
+
double iteration = floor(time / duration), progress = fmod(time, duration) / duration;
|
|
34
|
+
if ([input[@"reverse"] boolValue] && fmod(iteration, 2) >= 1) progress = 1 - progress;
|
|
35
|
+
NSUInteger lo = 0, hi = times.count - 1;
|
|
36
|
+
while (lo + 1 < hi) { NSUInteger mid = (lo + hi) / 2; if ([times[mid] doubleValue] <= progress) lo = mid; else hi = mid; }
|
|
37
|
+
double start = [times[lo] doubleValue], end = [times[hi] doubleValue];
|
|
38
|
+
double fraction = end > start ? (progress - start) / (end - start) : 1;
|
|
39
|
+
return [values[lo] doubleValue] + ([values[hi] doubleValue] - [values[lo] doubleValue]) * MAX(0, MIN(1, fraction));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
@interface JoltLiveSpring () {
|
|
43
|
+
JoltSpringState _state;
|
|
44
|
+
}
|
|
45
|
+
@property(nonatomic) double stiffness, damping, mass, restDelta, restSpeed;
|
|
46
|
+
@property(nonatomic, readwrite) BOOL moving;
|
|
47
|
+
@property(nonatomic) BOOL reduced, paused, disposed, direct, inputHeld;
|
|
48
|
+
@property(nonatomic) double targetValue;
|
|
49
|
+
@property(nonatomic, nullable) JoltLiveSpring *source;
|
|
50
|
+
@property(nonatomic) NSArray *sourceMaps;
|
|
51
|
+
@property(nonatomic) NSDictionary *input;
|
|
52
|
+
@property(nonatomic) double anchor;
|
|
53
|
+
@property(nonatomic) NSMutableArray<JoltLiveSpringBinding *> *bindings;
|
|
54
|
+
- (void)step:(double)now seconds:(double)seconds;
|
|
55
|
+
- (void)apply;
|
|
56
|
+
- (void)wake;
|
|
57
|
+
@end
|
|
58
|
+
@interface JoltLiveSpringBinding ()
|
|
59
|
+
@property(nonatomic, weak) UIView *view;
|
|
60
|
+
@property(nonatomic, weak) JoltLiveSpring *node;
|
|
61
|
+
@property(nonatomic) NSString *property;
|
|
62
|
+
@property(nonatomic) NSArray *mappings;
|
|
63
|
+
@property(nonatomic) BOOL stopped;
|
|
64
|
+
@property(nonatomic) NSArray *sampledLayers;
|
|
65
|
+
@property(nonatomic) double layerAnchor;
|
|
66
|
+
- (BOOL)running:(double)now;
|
|
67
|
+
- (void)apply;
|
|
68
|
+
@end
|
|
69
|
+
|
|
70
|
+
@implementation JoltLiveSpring
|
|
71
|
+
static NSMutableArray<JoltLiveSpring *> *nodes;
|
|
72
|
+
static CADisplayLink *displayLink;
|
|
73
|
+
static double previousFrame;
|
|
74
|
+
+ (void)initialize {
|
|
75
|
+
if (self == [JoltLiveSpring class]) nodes = [NSMutableArray new];
|
|
76
|
+
}
|
|
77
|
+
+ (void)schedule {
|
|
78
|
+
if (displayLink || !nodes.count) return;
|
|
79
|
+
displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(tick:)];
|
|
80
|
+
if (@available(iOS 15.0, *)) {
|
|
81
|
+
float maximum = MIN(120, UIScreen.mainScreen.maximumFramesPerSecond);
|
|
82
|
+
displayLink.preferredFrameRateRange = CAFrameRateRangeMake(MIN(80, maximum), maximum, maximum);
|
|
83
|
+
}
|
|
84
|
+
[displayLink addToRunLoop:NSRunLoop.mainRunLoop forMode:NSRunLoopCommonModes];
|
|
85
|
+
}
|
|
86
|
+
+ (void)tick:(CADisplayLink *)link {
|
|
87
|
+
double now = link.timestamp * 1000;
|
|
88
|
+
double remaining = previousFrame == 0 ? 1.0 / 120 : MIN(.064, MAX(0, link.timestamp - previousFrame));
|
|
89
|
+
previousFrame = link.timestamp;
|
|
90
|
+
while (remaining > 1e-9) {
|
|
91
|
+
double dt = MIN(remaining, 1.0 / 240);
|
|
92
|
+
remaining -= dt;
|
|
93
|
+
for (JoltLiveSpring *node in nodes) [node step:now - remaining * 1000 seconds:dt];
|
|
94
|
+
}
|
|
95
|
+
BOOL active = NO;
|
|
96
|
+
[CATransaction begin]; [CATransaction setDisableActions:YES];
|
|
97
|
+
for (JoltLiveSpring *node in nodes) {
|
|
98
|
+
[node apply]; active |= node.moving;
|
|
99
|
+
for (JoltLiveSpringBinding *binding in node.bindings) active |= [binding running:now];
|
|
100
|
+
}
|
|
101
|
+
[CATransaction commit];
|
|
102
|
+
if (!active) { [displayLink invalidate]; displayLink = nil; previousFrame = 0; }
|
|
103
|
+
}
|
|
104
|
+
+ (instancetype)create:(double)initial stiffness:(double)stiffness damping:(double)damping mass:(double)mass
|
|
105
|
+
restDelta:(double)restDelta restSpeed:(double)restSpeed reduced:(BOOL)reduced {
|
|
106
|
+
JoltLiveSpring *node = [self new];
|
|
107
|
+
node->_state = (JoltSpringState){initial, 0};
|
|
108
|
+
node.stiffness = stiffness; node.damping = damping; node.mass = mass;
|
|
109
|
+
node.restDelta = restDelta; node.restSpeed = restSpeed; node->_reduced = reduced;
|
|
110
|
+
node.direct = YES; node.targetValue = initial;
|
|
111
|
+
node.bindings = [NSMutableArray new]; node.sourceMaps = @[];
|
|
112
|
+
[nodes addObject:node];
|
|
113
|
+
return node;
|
|
114
|
+
}
|
|
115
|
+
- (double)position { return _state.position; }
|
|
116
|
+
- (double)velocity { return _state.velocity; }
|
|
117
|
+
+ (NSUInteger)activeCount { return displayLink ? 1 : 0; }
|
|
118
|
+
+ (NSUInteger)nodeCount { return nodes.count; }
|
|
119
|
+
- (void)follow:(JoltLiveSpring *)source mappings:(NSString *)mappings {
|
|
120
|
+
if (self.disposed || source.disposed || [nodes indexOfObject:source] >= [nodes indexOfObject:self])
|
|
121
|
+
[NSException raise:NSInvalidArgumentException format:@"A spring can only follow an existing live node"];
|
|
122
|
+
self.sourceMaps = decode(mappings); self.source = source; [self wake];
|
|
123
|
+
}
|
|
124
|
+
- (void)updateInput:(NSString *)snapshot {
|
|
125
|
+
if (self.disposed) return;
|
|
126
|
+
self.input = decode(snapshot); self.direct = NO; self.anchor = CACurrentMediaTime() * 1000; [self wake];
|
|
127
|
+
}
|
|
128
|
+
- (void)setTarget:(double)value {
|
|
129
|
+
if (self.disposed) return;
|
|
130
|
+
self.direct = YES; self.targetValue = value; self.input = nil; [self wake];
|
|
131
|
+
}
|
|
132
|
+
- (void)holdInput:(double)value velocity:(double)velocity {
|
|
133
|
+
if (self.disposed) return;
|
|
134
|
+
self.inputHeld = YES; self.direct = YES; self.targetValue = value; self.input = nil;
|
|
135
|
+
_state = (JoltSpringState){value, velocity}; self.moving = YES;
|
|
136
|
+
[CATransaction begin]; [CATransaction setDisableActions:YES]; [self apply]; [CATransaction commit];
|
|
137
|
+
[JoltLiveSpring schedule];
|
|
138
|
+
}
|
|
139
|
+
- (void)releaseInput:(double)target velocity:(double)velocity {
|
|
140
|
+
if (self.disposed) return;
|
|
141
|
+
self.inputHeld = NO; _state.velocity = velocity; [self setTarget:target];
|
|
142
|
+
}
|
|
143
|
+
- (void)setReduced:(BOOL)value { _reduced = value; [self wake]; }
|
|
144
|
+
- (void)setPaused:(BOOL)value {
|
|
145
|
+
_paused = value;
|
|
146
|
+
if (value) self.moving = NO; else [self wake];
|
|
147
|
+
}
|
|
148
|
+
- (void)wake {
|
|
149
|
+
if (self.disposed || self.paused) return;
|
|
150
|
+
self.moving = YES;
|
|
151
|
+
if (self.reduced) {
|
|
152
|
+
[self step:CACurrentMediaTime() * 1000 seconds:0];
|
|
153
|
+
[CATransaction begin]; [CATransaction setDisableActions:YES]; [self apply]; [CATransaction commit];
|
|
154
|
+
}
|
|
155
|
+
[JoltLiveSpring schedule];
|
|
156
|
+
}
|
|
157
|
+
- (void)step:(double)now seconds:(double)seconds {
|
|
158
|
+
if (self.disposed || self.paused || self.inputHeld) return;
|
|
159
|
+
double target = self.source ? mapped(self.source.position, self.sourceMaps) : (self.direct ? self.targetValue : inputValue(self.input, self.anchor, now));
|
|
160
|
+
BOOL inputMoving = self.source ? self.source.moving : (!self.direct && inputRunning(self.input, self.anchor, now));
|
|
161
|
+
if (self.reduced) _state = (JoltSpringState){target, 0};
|
|
162
|
+
else _state = JoltSpringAdvance(_state, target, self.stiffness, self.damping, self.mass, seconds);
|
|
163
|
+
BOOL rest = fabs(_state.position - target) <= self.restDelta && fabs(_state.velocity) <= self.restSpeed;
|
|
164
|
+
if (rest && !inputMoving) _state = (JoltSpringState){target, 0};
|
|
165
|
+
self.moving = inputMoving || !rest;
|
|
166
|
+
}
|
|
167
|
+
- (JoltLiveSpringBinding *)bind:(UIView *)view property:(NSString *)property mappings:(NSString *)mappings {
|
|
168
|
+
if (self.disposed) [NSException raise:NSInvalidArgumentException format:@"Spring disposed"];
|
|
169
|
+
JoltLiveSpringBinding *binding = [JoltLiveSpringBinding new];
|
|
170
|
+
binding.node = self; binding.view = view; binding.property = property; binding.mappings = decode(mappings);
|
|
171
|
+
[self.bindings addObject:binding];
|
|
172
|
+
[CATransaction begin]; [CATransaction setDisableActions:YES]; [binding apply]; [CATransaction commit];
|
|
173
|
+
return binding;
|
|
174
|
+
}
|
|
175
|
+
- (void)apply {
|
|
176
|
+
for (NSInteger i = self.bindings.count - 1; i >= 0; i--) {
|
|
177
|
+
JoltLiveSpringBinding *binding = self.bindings[i];
|
|
178
|
+
if (!binding.view) [self.bindings removeObjectAtIndex:i]; else [binding apply];
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
+ (void)sync:(UIView *)view {
|
|
182
|
+
[CATransaction begin]; [CATransaction setDisableActions:YES];
|
|
183
|
+
for (JoltLiveSpring *node in nodes)
|
|
184
|
+
for (JoltLiveSpringBinding *binding in node.bindings)
|
|
185
|
+
if (binding.view == view) [binding apply];
|
|
186
|
+
[CATransaction commit];
|
|
187
|
+
}
|
|
188
|
+
- (void)dispose {
|
|
189
|
+
if (self.disposed) return;
|
|
190
|
+
self.disposed = YES; self.moving = NO; [self.bindings removeAllObjects]; [nodes removeObject:self];
|
|
191
|
+
for (JoltLiveSpring *child in nodes) if (child.source == self) {
|
|
192
|
+
child.source = nil; [child setTarget:mapped(self.position, child.sourceMaps)];
|
|
193
|
+
}
|
|
194
|
+
self.source = nil;
|
|
195
|
+
if (!nodes.count) { [displayLink invalidate]; displayLink = nil; previousFrame = 0; }
|
|
196
|
+
}
|
|
197
|
+
@end
|
|
198
|
+
|
|
199
|
+
@implementation JoltLiveSpringBinding
|
|
200
|
+
- (double)value { return mapped(self.node.position, self.mappings); }
|
|
201
|
+
- (void)setLayers:(NSString *)inputs {
|
|
202
|
+
self.sampledLayers = decode(inputs); self.layerAnchor = CACurrentMediaTime() * 1000;
|
|
203
|
+
[CATransaction begin]; [CATransaction setDisableActions:YES]; [self apply]; [CATransaction commit];
|
|
204
|
+
[JoltLiveSpring schedule];
|
|
205
|
+
}
|
|
206
|
+
- (BOOL)running:(double)now {
|
|
207
|
+
for (NSDictionary *layer in self.sampledLayers) if (inputRunning(layer, self.layerAnchor, now)) return YES;
|
|
208
|
+
return NO;
|
|
209
|
+
}
|
|
210
|
+
- (void)cancel { self.stopped = YES; [self.node.bindings removeObject:self]; }
|
|
211
|
+
- (void)apply {
|
|
212
|
+
UIView *view = self.view;
|
|
213
|
+
if (self.stopped || !view.window) return;
|
|
214
|
+
NSString *key = self.property;
|
|
215
|
+
double value = self.value;
|
|
216
|
+
BOOL multiply = [key isEqualToString:@"opacity"] || [key hasPrefix:@"scale"];
|
|
217
|
+
double now = CACurrentMediaTime() * 1000;
|
|
218
|
+
for (NSDictionary *layer in self.sampledLayers) {
|
|
219
|
+
double part = inputValue(layer, self.layerAnchor, now);
|
|
220
|
+
value = multiply ? value * part : value + part;
|
|
221
|
+
}
|
|
222
|
+
if ([key isEqualToString:@"opacity"]) { view.alpha = MAX(0, MIN(1, value)); return; }
|
|
223
|
+
if ([key isEqualToString:@"x"]) key = @"transform.translation.x";
|
|
224
|
+
else if ([key isEqualToString:@"y"]) key = @"transform.translation.y";
|
|
225
|
+
else if ([key isEqualToString:@"scaleX"]) key = @"transform.scale.x";
|
|
226
|
+
else if ([key isEqualToString:@"scaleY"]) key = @"transform.scale.y";
|
|
227
|
+
else if ([key isEqualToString:@"rotate"]) { key = @"transform.rotation.z"; value *= M_PI / 180; }
|
|
228
|
+
else if ([key isEqualToString:@"rotateX"]) { key = @"transform.rotation.x"; value *= M_PI / 180; }
|
|
229
|
+
else if ([key isEqualToString:@"rotateY"]) { key = @"transform.rotation.y"; value *= M_PI / 180; }
|
|
230
|
+
[view.layer setValue:@(value) forKeyPath:key];
|
|
231
|
+
if ([view respondsToSelector:NSSelectorFromString(@"outerShadowContainerLayer")]) {
|
|
232
|
+
CALayer *shadow = [view valueForKey:@"outerShadowContainerLayer"];
|
|
233
|
+
[shadow setValue:@(value) forKeyPath:key];
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
@end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
#ifndef JOLT_SPRING_STATE_H
|
|
2
|
+
#define JOLT_SPRING_STATE_H
|
|
3
|
+
#include <math.h>
|
|
4
|
+
|
|
5
|
+
/* Exact constant-target solution. Position and velocity survive every retarget. */
|
|
6
|
+
typedef struct { double position, velocity; } JoltSpringState;
|
|
7
|
+
static inline JoltSpringState JoltSpringAdvance(JoltSpringState state, double target,
|
|
8
|
+
double stiffness, double damping, double mass, double seconds) {
|
|
9
|
+
if (seconds <= 0) return state;
|
|
10
|
+
const double x = state.position - target;
|
|
11
|
+
const double w = sqrt(stiffness / mass);
|
|
12
|
+
const double z = damping / (2 * sqrt(stiffness * mass));
|
|
13
|
+
double next, speed;
|
|
14
|
+
if (fabs(z - 1) < 1e-6) {
|
|
15
|
+
const double b = state.velocity + w * x, e = exp(-w * seconds);
|
|
16
|
+
next = e * (x + b * seconds);
|
|
17
|
+
speed = e * (b - w * (x + b * seconds));
|
|
18
|
+
} else if (z < 1) {
|
|
19
|
+
const double a = z * w, d = w * sqrt(1 - z * z);
|
|
20
|
+
const double b = (state.velocity + a * x) / d;
|
|
21
|
+
const double c = cos(d * seconds), s = sin(d * seconds), e = exp(-a * seconds);
|
|
22
|
+
next = e * (x * c + b * s);
|
|
23
|
+
speed = e * ((-a * x + b * d) * c + (-a * b - x * d) * s);
|
|
24
|
+
} else {
|
|
25
|
+
const double q = sqrt(z * z - 1);
|
|
26
|
+
/* Rationalized slow root avoids catastrophic cancellation at high damping. */
|
|
27
|
+
const double r1 = -w / (z + q), r2 = -w * (z + q);
|
|
28
|
+
const double c1 = (state.velocity - r2 * x) / (r1 - r2), c2 = x - c1;
|
|
29
|
+
const double e1 = exp(r1 * seconds), e2 = exp(r2 * seconds);
|
|
30
|
+
next = c1 * e1 + c2 * e2;
|
|
31
|
+
speed = c1 * r1 * e1 + c2 * r2 * e2;
|
|
32
|
+
}
|
|
33
|
+
return (JoltSpringState){target + next, speed};
|
|
34
|
+
}
|
|
35
|
+
#endif
|
package/src/android-plugin.d.ts
CHANGED
|
@@ -1,4 +1,63 @@
|
|
|
1
1
|
declare namespace org.rafaelaugusto.jolt {
|
|
2
|
+
class GestureSurface extends org.nativescript.widgets.GridLayout {
|
|
3
|
+
constructor(context: android.content.Context)
|
|
4
|
+
}
|
|
5
|
+
class GestureSession {
|
|
6
|
+
constructor(
|
|
7
|
+
surface: GestureSurface,
|
|
8
|
+
scroll: android.view.View | null,
|
|
9
|
+
x: LiveSpring,
|
|
10
|
+
y: LiveSpring,
|
|
11
|
+
scale: LiveSpring,
|
|
12
|
+
rotation: LiveSpring,
|
|
13
|
+
options: string,
|
|
14
|
+
listener: GestureSession.Listener,
|
|
15
|
+
)
|
|
16
|
+
isActive(): boolean
|
|
17
|
+
cancel(): void
|
|
18
|
+
setEnabled(value: boolean): void
|
|
19
|
+
settle(x: number, y: number, scale: number, rotation: number, animated: boolean): void
|
|
20
|
+
updateBounds(left: number, right: number, top: number, bottom: number): void
|
|
21
|
+
dispose(): void
|
|
22
|
+
}
|
|
23
|
+
namespace GestureSession {
|
|
24
|
+
class Listener {
|
|
25
|
+
constructor(implementation: {
|
|
26
|
+
onState(state: number, velocityX: number, velocityY: number): void
|
|
27
|
+
})
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
class LiveSpring {
|
|
31
|
+
constructor(
|
|
32
|
+
initial: number,
|
|
33
|
+
stiffness: number,
|
|
34
|
+
damping: number,
|
|
35
|
+
mass: number,
|
|
36
|
+
restDelta: number,
|
|
37
|
+
restSpeed: number,
|
|
38
|
+
reduced: boolean,
|
|
39
|
+
)
|
|
40
|
+
static activeCount(): number
|
|
41
|
+
static nodeCount(): number
|
|
42
|
+
static sync(view: android.view.View): void
|
|
43
|
+
getPosition(): number
|
|
44
|
+
getVelocity(): number
|
|
45
|
+
isMoving(): boolean
|
|
46
|
+
follow(source: LiveSpring, mappings: string): void
|
|
47
|
+
updateInput(snapshot: string): void
|
|
48
|
+
setTarget(value: number): void
|
|
49
|
+
setReduced(value: boolean): void
|
|
50
|
+
setPaused(value: boolean): void
|
|
51
|
+
bind(view: android.view.View, property: string, mappings: string): LiveSpring.Binding
|
|
52
|
+
dispose(): void
|
|
53
|
+
}
|
|
54
|
+
namespace LiveSpring {
|
|
55
|
+
class Binding {
|
|
56
|
+
getValue(): number
|
|
57
|
+
setLayers(inputs: string): void
|
|
58
|
+
cancel(): void
|
|
59
|
+
}
|
|
60
|
+
}
|
|
2
61
|
class ParticleEmitter {
|
|
3
62
|
constructor(
|
|
4
63
|
target: android.view.View,
|
package/src/drag.ts
CHANGED
|
@@ -428,7 +428,7 @@ class DragController implements Drag {
|
|
|
428
428
|
const [axis] = this.config.axes
|
|
429
429
|
const value = this.config.value
|
|
430
430
|
const animation = value
|
|
431
|
-
? value.
|
|
431
|
+
? value.playTrack(createTrack(axis!, [event[axis!]], plans[0]!.transition, 0))
|
|
432
432
|
: new ViewAnimation(this.view, plans)
|
|
433
433
|
this.release = animation
|
|
434
434
|
void animation.finished.then((finished) => {
|
package/src/driver/android.ts
CHANGED
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
type AndroidPlayback,
|
|
5
5
|
} from './android-playback.js'
|
|
6
6
|
import { isDecorationProperty } from '../decoration.js'
|
|
7
|
+
import { syncLiveSprings } from '../live-spring-driver.js'
|
|
7
8
|
import {
|
|
8
9
|
syncAndroidDecoration,
|
|
9
10
|
playAndroidDecoration,
|
|
@@ -188,6 +189,7 @@ export function createAndroidDriver(): Driver {
|
|
|
188
189
|
sync(view) {
|
|
189
190
|
syncAndroidDecoration(view)
|
|
190
191
|
syncAndroidColor(view)
|
|
192
|
+
syncLiveSprings(view)
|
|
191
193
|
},
|
|
192
194
|
|
|
193
195
|
elapsed: (handle) => (handle as Handle).elapsed(),
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { ScrollView } from '@nativescript/core'
|
|
2
|
+
|
|
3
|
+
/** Preserve Core's lazy scroll listener attach/detach without adding a JS scroll observer. */
|
|
4
|
+
export function retainScrollDelegate(scroll: ScrollView, refresh: () => void) {
|
|
5
|
+
const host = scroll as ScrollView & { attachNative(): void; detachNative(): void }
|
|
6
|
+
const restore: (() => void)[] = []
|
|
7
|
+
for (const key of ['attachNative', 'detachNative'] as const) {
|
|
8
|
+
const original = host[key]
|
|
9
|
+
if (typeof original !== 'function') continue
|
|
10
|
+
const descriptor = Object.getOwnPropertyDescriptor(host, key)
|
|
11
|
+
const wrapper = function (this: typeof host) {
|
|
12
|
+
original.call(this)
|
|
13
|
+
refresh()
|
|
14
|
+
}
|
|
15
|
+
host[key] = wrapper
|
|
16
|
+
restore.push(() => {
|
|
17
|
+
if (host[key] !== wrapper) return
|
|
18
|
+
if (descriptor) Object.defineProperty(host, key, descriptor)
|
|
19
|
+
else delete (host as Partial<typeof host>)[key]
|
|
20
|
+
})
|
|
21
|
+
}
|
|
22
|
+
return () => restore.forEach((action) => action())
|
|
23
|
+
}
|
package/src/driver/ios.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { iosPlayback } from './ios-playback.js'
|
|
2
|
+
import { syncLiveSprings } from '../live-spring-driver.js'
|
|
2
3
|
import { isDecorationProperty } from '../decoration.js'
|
|
3
4
|
import {
|
|
4
5
|
playIOSDecoration,
|
|
@@ -197,6 +198,7 @@ export function createIOSDriver(): Driver {
|
|
|
197
198
|
view.nativeViewProtected,
|
|
198
199
|
excluding ? (keyPaths[excluding as NumericProperty] ?? null) : null,
|
|
199
200
|
)
|
|
201
|
+
syncLiveSprings(view)
|
|
200
202
|
},
|
|
201
203
|
|
|
202
204
|
presented(view, property) {
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { isIOS, type View, type ScrollView } from '@nativescript/core'
|
|
2
|
+
import { liveValueInput, type DerivedValue } from './motion-value.js'
|
|
3
|
+
import { nativeLiveNode } from './live-spring-driver.js'
|
|
4
|
+
import { retainScrollDelegate } from './driver/ios-scroll-delegate.js'
|
|
5
|
+
|
|
6
|
+
export interface GestureSettings {
|
|
7
|
+
axis: number
|
|
8
|
+
threshold: number
|
|
9
|
+
failOffset: number
|
|
10
|
+
pinch: boolean
|
|
11
|
+
rotate: boolean
|
|
12
|
+
tap: boolean
|
|
13
|
+
doubleTap: boolean
|
|
14
|
+
minX: number
|
|
15
|
+
maxX: number
|
|
16
|
+
minY: number
|
|
17
|
+
maxY: number
|
|
18
|
+
minScale: number
|
|
19
|
+
maxScale: number
|
|
20
|
+
}
|
|
21
|
+
export interface GestureDriver {
|
|
22
|
+
readonly active: boolean
|
|
23
|
+
cancel(): void
|
|
24
|
+
enabled(value: boolean): void
|
|
25
|
+
settle(values: readonly number[], animated: boolean): void
|
|
26
|
+
bounds(settings: GestureSettings): void
|
|
27
|
+
dispose(): void
|
|
28
|
+
}
|
|
29
|
+
export function createGestureDriver(
|
|
30
|
+
surface: View,
|
|
31
|
+
scroll: ScrollView | undefined,
|
|
32
|
+
values: readonly DerivedValue[],
|
|
33
|
+
options: GestureSettings,
|
|
34
|
+
callback: (state: number, vx: number, vy: number) => void,
|
|
35
|
+
): GestureDriver {
|
|
36
|
+
const inputs = values.map(liveValueInput)
|
|
37
|
+
const nodes = inputs.map((input) => nativeLiveNode(input.node))
|
|
38
|
+
const changed = (state: number, vx: number, vy: number) => {
|
|
39
|
+
for (const input of inputs) input.changed()
|
|
40
|
+
callback(state, vx, vy)
|
|
41
|
+
}
|
|
42
|
+
const json = JSON.stringify(options)
|
|
43
|
+
if (isIOS) {
|
|
44
|
+
if (typeof JoltGestureSession === 'undefined')
|
|
45
|
+
throw new Error('Rebuild the native app to use gestures().')
|
|
46
|
+
const [x, y, scale, rotation] = nodes as [
|
|
47
|
+
JoltLiveSpring,
|
|
48
|
+
JoltLiveSpring,
|
|
49
|
+
JoltLiveSpring,
|
|
50
|
+
JoltLiveSpring,
|
|
51
|
+
]
|
|
52
|
+
const native = JoltGestureSession.attachScrollXYScaleRotationOptionsOnState(
|
|
53
|
+
surface.nativeViewProtected,
|
|
54
|
+
scroll?.nativeViewProtected ?? null,
|
|
55
|
+
x,
|
|
56
|
+
y,
|
|
57
|
+
scale,
|
|
58
|
+
rotation,
|
|
59
|
+
json,
|
|
60
|
+
changed,
|
|
61
|
+
)
|
|
62
|
+
const releaseDelegate = scroll
|
|
63
|
+
? retainScrollDelegate(scroll, () => native.refreshScrollDelegate())
|
|
64
|
+
: () => {}
|
|
65
|
+
return {
|
|
66
|
+
get active() {
|
|
67
|
+
return native.active
|
|
68
|
+
},
|
|
69
|
+
cancel: () => native.cancel(),
|
|
70
|
+
enabled: (value) => native.setEnabled(value),
|
|
71
|
+
settle: (values, animated) => {
|
|
72
|
+
native.settleXYScaleRotationAnimated(
|
|
73
|
+
values[0]!,
|
|
74
|
+
values[1]!,
|
|
75
|
+
values[2]!,
|
|
76
|
+
values[3]!,
|
|
77
|
+
animated,
|
|
78
|
+
)
|
|
79
|
+
inputs.forEach((input) => input.changed())
|
|
80
|
+
},
|
|
81
|
+
bounds: (settings) =>
|
|
82
|
+
native.updateLeftRightTopBottom(
|
|
83
|
+
settings.minX,
|
|
84
|
+
settings.maxX,
|
|
85
|
+
settings.minY,
|
|
86
|
+
settings.maxY,
|
|
87
|
+
),
|
|
88
|
+
dispose: () => {
|
|
89
|
+
releaseDelegate()
|
|
90
|
+
native.dispose()
|
|
91
|
+
},
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
const [x, y, scale, rotation] = nodes as [
|
|
95
|
+
org.rafaelaugusto.jolt.LiveSpring,
|
|
96
|
+
org.rafaelaugusto.jolt.LiveSpring,
|
|
97
|
+
org.rafaelaugusto.jolt.LiveSpring,
|
|
98
|
+
org.rafaelaugusto.jolt.LiveSpring,
|
|
99
|
+
]
|
|
100
|
+
const native = new org.rafaelaugusto.jolt.GestureSession(
|
|
101
|
+
surface.nativeViewProtected,
|
|
102
|
+
scroll?.nativeViewProtected ?? null,
|
|
103
|
+
x,
|
|
104
|
+
y,
|
|
105
|
+
scale,
|
|
106
|
+
rotation,
|
|
107
|
+
json,
|
|
108
|
+
new org.rafaelaugusto.jolt.GestureSession.Listener({ onState: changed }),
|
|
109
|
+
)
|
|
110
|
+
return {
|
|
111
|
+
get active() {
|
|
112
|
+
return native.isActive()
|
|
113
|
+
},
|
|
114
|
+
cancel: () => native.cancel(),
|
|
115
|
+
enabled: (value) => native.setEnabled(value),
|
|
116
|
+
settle: (values, animated) => {
|
|
117
|
+
native.settle(values[0]!, values[1]!, values[2]!, values[3]!, animated)
|
|
118
|
+
inputs.forEach((input) => input.changed())
|
|
119
|
+
},
|
|
120
|
+
bounds: (settings) =>
|
|
121
|
+
native.updateBounds(settings.minX, settings.maxX, settings.minY, settings.maxY),
|
|
122
|
+
dispose: () => native.dispose(),
|
|
123
|
+
}
|
|
124
|
+
}
|