@rafaelaugusto/jolt 0.1.0-rc.2 → 0.1.0-rc.4

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.
Files changed (91) hide show
  1. package/README.md +4 -4
  2. package/dist/animate.d.ts.map +1 -1
  3. package/dist/animate.js +20 -5
  4. package/dist/animate.js.map +1 -1
  5. package/dist/bottom-sheet/geometry.d.ts +7 -2
  6. package/dist/bottom-sheet/geometry.d.ts.map +1 -1
  7. package/dist/bottom-sheet/geometry.js +7 -3
  8. package/dist/bottom-sheet/geometry.js.map +1 -1
  9. package/dist/bottom-sheet/index.d.ts +10 -0
  10. package/dist/bottom-sheet/index.d.ts.map +1 -1
  11. package/dist/bottom-sheet/index.js +72 -22
  12. package/dist/bottom-sheet/index.js.map +1 -1
  13. package/dist/bottom-sheet/native.d.ts +5 -0
  14. package/dist/bottom-sheet/native.d.ts.map +1 -1
  15. package/dist/bottom-sheet/native.js +6 -0
  16. package/dist/bottom-sheet/native.js.map +1 -1
  17. package/dist/driver/android.d.ts.map +1 -1
  18. package/dist/driver/android.js +15 -7
  19. package/dist/driver/android.js.map +1 -1
  20. package/dist/index.d.ts +1 -1
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +1 -1
  23. package/dist/index.js.map +1 -1
  24. package/dist/interactive-navigation/index.d.ts +2 -0
  25. package/dist/interactive-navigation/index.d.ts.map +1 -1
  26. package/dist/interactive-navigation/index.js +1 -0
  27. package/dist/interactive-navigation/index.js.map +1 -1
  28. package/dist/interactive-navigation/modal.d.ts +34 -0
  29. package/dist/interactive-navigation/modal.d.ts.map +1 -0
  30. package/dist/interactive-navigation/modal.js +253 -0
  31. package/dist/interactive-navigation/modal.js.map +1 -0
  32. package/dist/live-spring-driver.d.ts +5 -0
  33. package/dist/live-spring-driver.d.ts.map +1 -1
  34. package/dist/live-spring-driver.js +13 -0
  35. package/dist/live-spring-driver.js.map +1 -1
  36. package/dist/mixer.d.ts +7 -1
  37. package/dist/mixer.d.ts.map +1 -1
  38. package/dist/mixer.js +57 -22
  39. package/dist/mixer.js.map +1 -1
  40. package/dist/motion-value.d.ts +12 -1
  41. package/dist/motion-value.d.ts.map +1 -1
  42. package/dist/motion-value.js +115 -7
  43. package/dist/motion-value.js.map +1 -1
  44. package/dist/scroll-header/geometry.d.ts +1 -1
  45. package/dist/scroll-header/geometry.d.ts.map +1 -1
  46. package/dist/scroll-header/geometry.js +7 -2
  47. package/dist/scroll-header/geometry.js.map +1 -1
  48. package/dist/scroll-header/index.d.ts.map +1 -1
  49. package/dist/scroll-header/index.js +13 -0
  50. package/dist/scroll-header/index.js.map +1 -1
  51. package/dist/shapes/index.d.ts +1 -1
  52. package/dist/shapes/index.d.ts.map +1 -1
  53. package/dist/shapes/index.js +23 -9
  54. package/dist/shapes/index.js.map +1 -1
  55. package/dist/types.d.ts +5 -1
  56. package/dist/types.d.ts.map +1 -1
  57. package/dist/value-track.d.ts +9 -0
  58. package/dist/value-track.d.ts.map +1 -1
  59. package/dist/value-track.js +28 -16
  60. package/dist/value-track.js.map +1 -1
  61. package/package.json +1 -1
  62. package/platforms/android/java/org/rafaelaugusto/jolt/BlurEffect.java +14 -2
  63. package/platforms/android/java/org/rafaelaugusto/jolt/GradientLayer.java +30 -8
  64. package/platforms/android/java/org/rafaelaugusto/jolt/KeyframeFactory.java +26 -0
  65. package/platforms/android/java/org/rafaelaugusto/jolt/LiveSpring.java +36 -9
  66. package/platforms/android/java/org/rafaelaugusto/jolt/ScrollHeader.java +3 -2
  67. package/platforms/android/java/org/rafaelaugusto/jolt/ShapeLayer.java +17 -7
  68. package/platforms/android/proguard-rules.pro +1 -0
  69. package/platforms/ios/src/JoltInteractiveModal.h +16 -0
  70. package/platforms/ios/src/JoltInteractiveModal.m +178 -0
  71. package/platforms/ios/src/JoltLiveSpring.h +1 -0
  72. package/platforms/ios/src/JoltLiveSpring.m +183 -67
  73. package/platforms/ios/src/module.modulemap +1 -0
  74. package/src/android-plugin.d.ts +10 -0
  75. package/src/animate.ts +23 -5
  76. package/src/bottom-sheet/geometry.ts +9 -2
  77. package/src/bottom-sheet/index.ts +94 -20
  78. package/src/bottom-sheet/native.ts +6 -0
  79. package/src/driver/android.ts +31 -14
  80. package/src/index.ts +1 -1
  81. package/src/interactive-navigation/index.ts +7 -0
  82. package/src/interactive-navigation/modal.ts +306 -0
  83. package/src/ios-plugin.d.ts +23 -0
  84. package/src/live-spring-driver.ts +15 -0
  85. package/src/mixer.ts +62 -22
  86. package/src/motion-value.ts +121 -9
  87. package/src/scroll-header/geometry.ts +7 -2
  88. package/src/scroll-header/index.ts +12 -0
  89. package/src/shapes/index.ts +20 -9
  90. package/src/types.ts +5 -2
  91. package/src/value-track.ts +25 -14
@@ -2,67 +2,133 @@
2
2
  #import "JoltSpringState.h"
3
3
  #import <QuartzCore/QuartzCore.h>
4
4
 
5
+ /* Sampled inputs and mappings are decoded once; frames read plain C arrays. */
6
+ typedef struct {
7
+ double value, elapsed, duration, delay, repeat, to;
8
+ BOOL reverse, playing;
9
+ NSUInteger count;
10
+ double *times, *values;
11
+ } JoltSampled;
12
+ typedef struct {
13
+ NSUInteger count;
14
+ double *input, *output;
15
+ BOOL clamp;
16
+ } JoltMapping;
17
+
5
18
  static id decode(NSString *json) {
6
19
  return [NSJSONSerialization JSONObjectWithData:[json dataUsingEncoding:NSUTF8StringEncoding] options:0 error:nil];
7
20
  }
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;
21
+ static double *doubles(NSArray *numbers, NSUInteger count) {
22
+ double *result = malloc(sizeof(double) * MAX(1, count));
23
+ for (NSUInteger i = 0; i < count; i++) result[i] = [numbers[i] doubleValue];
24
+ return result;
25
+ }
26
+ static JoltSampled *decodeSampled(NSArray *list, NSUInteger *count) {
27
+ *count = list.count;
28
+ if (!*count) return NULL;
29
+ JoltSampled *result = calloc(*count, sizeof(JoltSampled));
30
+ for (NSUInteger i = 0; i < *count; i++) {
31
+ NSDictionary *input = list[i];
32
+ NSArray *values = input[@"values"], *times = input[@"times"];
33
+ JoltSampled *s = &result[i];
34
+ s->value = [input[@"value"] doubleValue]; s->elapsed = [input[@"elapsed"] doubleValue];
35
+ s->duration = [input[@"duration"] doubleValue]; s->delay = [input[@"delay"] doubleValue];
36
+ s->repeat = [input[@"repeat"] doubleValue]; s->to = [input[@"to"] doubleValue];
37
+ s->reverse = [input[@"reverse"] boolValue]; s->playing = [input[@"running"] boolValue];
38
+ s->count = MIN(values.count, times.count);
39
+ s->times = doubles(times, s->count);
40
+ s->values = doubles(values, s->count);
19
41
  }
20
- return value;
42
+ return result;
21
43
  }
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]);
44
+ static void freeSampled(JoltSampled *list, NSUInteger count) {
45
+ for (NSUInteger i = 0; i < count; i++) { free(list[i].times); free(list[i].values); }
46
+ free(list);
47
+ }
48
+ static BOOL sampledRunning(const JoltSampled *s, double anchor, double now) {
49
+ return s->playing && (s->repeat < 0 || now - anchor + s->elapsed < s->delay + s->duration * (s->repeat + 1));
50
+ }
51
+ static double sampledValue(const JoltSampled *s, double anchor, double now) {
52
+ if (!s->playing || s->count < 2) return s->value;
53
+ if (!sampledRunning(s, anchor, now)) return s->to;
54
+ double time = MAX(0, now - anchor + s->elapsed - s->delay);
55
+ double duration = MAX(1, s->duration);
33
56
  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];
57
+ if (s->reverse && fmod(iteration, 2) >= 1) progress = 1 - progress;
58
+ NSUInteger lo = 0, hi = s->count - 1;
59
+ while (lo + 1 < hi) { NSUInteger mid = (lo + hi) / 2; if (s->times[mid] <= progress) lo = mid; else hi = mid; }
60
+ double start = s->times[lo], end = s->times[hi];
38
61
  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));
62
+ return s->values[lo] + (s->values[hi] - s->values[lo]) * MAX(0, MIN(1, fraction));
40
63
  }
64
+ static JoltMapping *decodeMappings(NSString *json, NSUInteger *count) {
65
+ NSArray *list = decode(json);
66
+ *count = list.count;
67
+ if (!*count) return NULL;
68
+ JoltMapping *result = calloc(*count, sizeof(JoltMapping));
69
+ for (NSUInteger i = 0; i < *count; i++) {
70
+ NSDictionary *mapping = list[i];
71
+ NSArray *input = mapping[@"input"], *output = mapping[@"output"];
72
+ result[i].count = MIN(input.count, output.count);
73
+ result[i].input = doubles(input, result[i].count);
74
+ result[i].output = doubles(output, result[i].count);
75
+ result[i].clamp = [mapping[@"clamp"] boolValue];
76
+ }
77
+ return result;
78
+ }
79
+ static void freeMappings(JoltMapping *maps, NSUInteger count) {
80
+ for (NSUInteger i = 0; i < count; i++) { free(maps[i].input); free(maps[i].output); }
81
+ free(maps);
82
+ }
83
+ static double mapped(double value, const JoltMapping *maps, NSUInteger count) {
84
+ for (NSUInteger m = 0; m < count; m++) {
85
+ const JoltMapping *mapping = &maps[m];
86
+ if (mapping->count < 2) continue;
87
+ if (mapping->clamp && value <= mapping->input[0]) { value = mapping->output[0]; continue; }
88
+ if (mapping->clamp && value >= mapping->input[mapping->count - 1]) { value = mapping->output[mapping->count - 1]; continue; }
89
+ NSUInteger index = 0;
90
+ while (index < mapping->count - 2 && value > mapping->input[index + 1]) index++;
91
+ double span = mapping->input[index + 1] - mapping->input[index];
92
+ double fraction = span > 0 ? (value - mapping->input[index]) / span : value < mapping->input[index] ? 0 : 1;
93
+ value = mapping->output[index] + (mapping->output[index + 1] - mapping->output[index]) * fraction;
94
+ }
95
+ return value;
96
+ }
97
+
98
+ @protocol JoltShadowedView
99
+ - (CALayer *)outerShadowContainerLayer;
100
+ @end
41
101
 
42
102
  @interface JoltLiveSpring () {
43
103
  JoltSpringState _state;
104
+ JoltSampled *_inputs;
105
+ NSUInteger _inputCount;
106
+ JoltMapping *_sourceMaps;
107
+ NSUInteger _sourceMapCount;
44
108
  }
45
109
  @property(nonatomic) double stiffness, damping, mass, restDelta, restSpeed;
46
110
  @property(nonatomic, readwrite) BOOL moving;
47
111
  @property(nonatomic) BOOL reduced, paused, disposed, direct, inputHeld, sensorDriven;
48
112
  @property(nonatomic) double targetValue;
49
113
  @property(nonatomic, nullable) JoltLiveSpring *source;
50
- @property(nonatomic) NSArray *sourceMaps;
51
- @property(nonatomic) NSDictionary *input;
52
- @property(nonatomic) double anchor;
114
+ @property(nonatomic) double inputRest, anchor;
53
115
  @property(nonatomic) NSMutableArray<JoltLiveSpringBinding *> *bindings;
54
116
  - (void)step:(double)now seconds:(double)seconds;
55
117
  - (void)apply;
56
118
  - (void)wake;
57
119
  @end
58
- @interface JoltLiveSpringBinding ()
120
+ @interface JoltLiveSpringBinding () {
121
+ JoltMapping *_maps;
122
+ NSUInteger _mapCount;
123
+ JoltSampled *_layers;
124
+ NSUInteger _layerCount;
125
+ }
59
126
  @property(nonatomic, weak) UIView *view;
60
127
  @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;
128
+ @property(nonatomic) NSString *property, *keyPath;
129
+ @property(nonatomic) BOOL stopped, multiply, radians, alpha, shadowed;
65
130
  @property(nonatomic) double layerAnchor;
131
+ - (void)useMappings:(NSString *)mappings;
66
132
  - (BOOL)running:(double)now;
67
133
  - (void)apply;
68
134
  @end
@@ -90,7 +156,9 @@ static double previousFrame;
90
156
  while (remaining > 1e-9) {
91
157
  double dt = MIN(remaining, 1.0 / 240);
92
158
  remaining -= dt;
93
- for (JoltLiveSpring *node in nodes) [node step:now - remaining * 1000 seconds:dt];
159
+ // An instant node only needs the frame's final time; springs integrate every substep.
160
+ for (JoltLiveSpring *node in nodes)
161
+ if (!node.reduced || remaining <= 1e-9) [node step:now - remaining * 1000 seconds:dt];
94
162
  }
95
163
  BOOL active = NO;
96
164
  [CATransaction begin]; [CATransaction setDisableActions:YES];
@@ -108,36 +176,68 @@ static double previousFrame;
108
176
  node.stiffness = stiffness; node.damping = damping; node.mass = mass;
109
177
  node.restDelta = restDelta; node.restSpeed = restSpeed; node->_reduced = reduced;
110
178
  node.direct = YES; node.targetValue = initial;
111
- node.bindings = [NSMutableArray new]; node.sourceMaps = @[];
179
+ node.bindings = [NSMutableArray new];
112
180
  [nodes addObject:node];
113
181
  return node;
114
182
  }
183
+ - (void)dealloc {
184
+ freeSampled(_inputs, _inputCount);
185
+ freeMappings(_sourceMaps, _sourceMapCount);
186
+ }
115
187
  - (double)position { return _state.position; }
116
188
  - (double)velocity { return _state.velocity; }
117
189
  + (NSUInteger)activeCount { return displayLink ? 1 : 0; }
118
190
  + (NSUInteger)nodeCount { return nodes.count; }
191
+ - (void)replaceInputs:(NSArray *)list rest:(double)rest {
192
+ freeSampled(_inputs, _inputCount);
193
+ _inputs = decodeSampled(list, &_inputCount);
194
+ self.inputRest = rest; self.anchor = CACurrentMediaTime() * 1000;
195
+ }
119
196
  - (void)follow:(JoltLiveSpring *)source mappings:(NSString *)mappings {
120
197
  if (self.disposed || source.disposed || [nodes indexOfObject:source] >= [nodes indexOfObject:self])
121
198
  [NSException raise:NSInvalidArgumentException format:@"A spring can only follow an existing live node"];
122
- self.sourceMaps = decode(mappings); self.source = source; [self wake];
199
+ freeMappings(_sourceMaps, _sourceMapCount);
200
+ _sourceMaps = decodeMappings(mappings, &_sourceMapCount);
201
+ self.source = source; [self wake];
123
202
  }
124
203
  - (void)updateInput:(NSString *)snapshot {
125
204
  if (self.disposed) return;
126
- self.sensorDriven = NO; self.input = decode(snapshot); self.direct = NO; self.anchor = CACurrentMediaTime() * 1000; [self wake];
205
+ self.sensorDriven = NO; self.direct = NO;
206
+ [self replaceInputs:@[decode(snapshot)] rest:0];
207
+ [self wake];
208
+ }
209
+ - (void)setInputs:(NSString *)snapshots rest:(double)rest {
210
+ if (self.disposed) return;
211
+ self.sensorDriven = NO; self.direct = NO;
212
+ [self replaceInputs:decode(snapshots) rest:rest];
213
+ [self wake];
214
+ }
215
+ - (double)inputValue:(double)now {
216
+ double value = self.inputRest;
217
+ for (NSUInteger i = 0; i < _inputCount; i++) value += sampledValue(&_inputs[i], self.anchor, now);
218
+ return value;
219
+ }
220
+ - (BOOL)inputMoving:(double)now {
221
+ for (NSUInteger i = 0; i < _inputCount; i++) if (sampledRunning(&_inputs[i], self.anchor, now)) return YES;
222
+ return NO;
127
223
  }
128
224
  - (void)setSensorInput:(double)value {
129
225
  if (self.disposed || !isfinite(value) || (self.sensorDriven && self.inputHeld && _state.position == value)) return;
130
- self.sensorDriven = YES; self.inputHeld = YES; self.direct = YES; self.targetValue = value; self.input = nil;
226
+ self.sensorDriven = YES; self.inputHeld = YES; self.direct = YES; self.targetValue = value;
227
+ [self replaceInputs:@[] rest:0];
131
228
  _state = (JoltSpringState){value, 0}; self.moving = NO;
132
229
  [JoltLiveSpring schedule];
133
230
  }
134
231
  - (void)setTarget:(double)value {
135
232
  if (self.disposed) return;
136
- self.sensorDriven = NO; self.direct = YES; self.targetValue = value; self.input = nil; [self wake];
233
+ self.sensorDriven = NO; self.direct = YES; self.targetValue = value;
234
+ [self replaceInputs:@[] rest:0];
235
+ [self wake];
137
236
  }
138
237
  - (void)holdInput:(double)value velocity:(double)velocity {
139
238
  if (self.disposed) return;
140
- self.sensorDriven = NO; self.inputHeld = YES; self.direct = YES; self.targetValue = value; self.input = nil;
239
+ self.sensorDriven = NO; self.inputHeld = YES; self.direct = YES; self.targetValue = value;
240
+ [self replaceInputs:@[] rest:0];
141
241
  _state = (JoltSpringState){value, velocity}; self.moving = YES;
142
242
  [CATransaction begin]; [CATransaction setDisableActions:YES]; [self apply]; [CATransaction commit];
143
243
  [JoltLiveSpring schedule];
@@ -163,8 +263,8 @@ static double previousFrame;
163
263
  }
164
264
  - (void)step:(double)now seconds:(double)seconds {
165
265
  if (self.disposed || self.paused || self.inputHeld) return;
166
- double target = self.source ? mapped(self.source.position, self.sourceMaps) : (self.direct ? self.targetValue : inputValue(self.input, self.anchor, now));
167
- BOOL inputMoving = self.source ? self.source.moving : (!self.direct && inputRunning(self.input, self.anchor, now));
266
+ double target = self.source ? mapped(self.source.position, _sourceMaps, _sourceMapCount) : (self.direct ? self.targetValue : [self inputValue:now]);
267
+ BOOL inputMoving = self.source ? self.source.moving : (!self.direct && [self inputMoving:now]);
168
268
  if (self.reduced) _state = (JoltSpringState){target, 0};
169
269
  else _state = JoltSpringAdvance(_state, target, self.stiffness, self.damping, self.mass, seconds);
170
270
  BOOL rest = fabs(_state.position - target) <= self.restDelta && fabs(_state.velocity) <= self.restSpeed;
@@ -174,7 +274,20 @@ static double previousFrame;
174
274
  - (JoltLiveSpringBinding *)bind:(UIView *)view property:(NSString *)property mappings:(NSString *)mappings {
175
275
  if (self.disposed) [NSException raise:NSInvalidArgumentException format:@"Spring disposed"];
176
276
  JoltLiveSpringBinding *binding = [JoltLiveSpringBinding new];
177
- binding.node = self; binding.view = view; binding.property = property; binding.mappings = decode(mappings);
277
+ binding.node = self; binding.view = view; binding.property = property;
278
+ [binding useMappings:mappings];
279
+ NSString *key = property;
280
+ if ([key isEqualToString:@"opacity"]) binding.alpha = YES;
281
+ else if ([key isEqualToString:@"x"]) key = @"transform.translation.x";
282
+ else if ([key isEqualToString:@"y"]) key = @"transform.translation.y";
283
+ else if ([key isEqualToString:@"scaleX"]) key = @"transform.scale.x";
284
+ else if ([key isEqualToString:@"scaleY"]) key = @"transform.scale.y";
285
+ else if ([key isEqualToString:@"rotate"]) { key = @"transform.rotation.z"; binding.radians = YES; }
286
+ else if ([key isEqualToString:@"rotateX"]) { key = @"transform.rotation.x"; binding.radians = YES; }
287
+ else if ([key isEqualToString:@"rotateY"]) { key = @"transform.rotation.y"; binding.radians = YES; }
288
+ binding.keyPath = key;
289
+ binding.multiply = binding.alpha || [property hasPrefix:@"scale"];
290
+ binding.shadowed = [view respondsToSelector:@selector(outerShadowContainerLayer)];
178
291
  [self.bindings addObject:binding];
179
292
  [CATransaction begin]; [CATransaction setDisableActions:YES]; [binding apply]; [CATransaction commit];
180
293
  return binding;
@@ -196,7 +309,7 @@ static double previousFrame;
196
309
  if (self.disposed) return;
197
310
  self.disposed = YES; self.moving = NO; [self.bindings removeAllObjects]; [nodes removeObject:self];
198
311
  for (JoltLiveSpring *child in nodes) if (child.source == self) {
199
- child.source = nil; [child setTarget:mapped(self.position, child.sourceMaps)];
312
+ child.source = nil; [child setTarget:mapped(self.position, child->_sourceMaps, child->_sourceMapCount)];
200
313
  }
201
314
  self.source = nil;
202
315
  if (!nodes.count) { [displayLink invalidate]; displayLink = nil; previousFrame = 0; }
@@ -204,40 +317,43 @@ static double previousFrame;
204
317
  @end
205
318
 
206
319
  @implementation JoltLiveSpringBinding
207
- - (double)value { return mapped(self.node.position, self.mappings); }
320
+ - (void)dealloc {
321
+ freeMappings(_maps, _mapCount);
322
+ freeSampled(_layers, _layerCount);
323
+ }
324
+ - (void)useMappings:(NSString *)mappings {
325
+ freeMappings(_maps, _mapCount);
326
+ _maps = decodeMappings(mappings, &_mapCount);
327
+ }
328
+ - (double)value { return mapped(self.node.position, _maps, _mapCount); }
208
329
  - (void)setLayers:(NSString *)inputs {
209
- self.sampledLayers = decode(inputs); self.layerAnchor = CACurrentMediaTime() * 1000;
330
+ freeSampled(_layers, _layerCount);
331
+ _layers = decodeSampled(decode(inputs), &_layerCount);
332
+ self.layerAnchor = CACurrentMediaTime() * 1000;
210
333
  [CATransaction begin]; [CATransaction setDisableActions:YES]; [self apply]; [CATransaction commit];
211
334
  [JoltLiveSpring schedule];
212
335
  }
213
336
  - (BOOL)running:(double)now {
214
- for (NSDictionary *layer in self.sampledLayers) if (inputRunning(layer, self.layerAnchor, now)) return YES;
337
+ for (NSUInteger i = 0; i < _layerCount; i++) if (sampledRunning(&_layers[i], self.layerAnchor, now)) return YES;
215
338
  return NO;
216
339
  }
217
340
  - (void)cancel { self.stopped = YES; [self.node.bindings removeObject:self]; }
218
341
  - (void)apply {
219
342
  UIView *view = self.view;
220
343
  if (self.stopped || !view.window) return;
221
- NSString *key = self.property;
222
344
  double value = self.value;
223
- BOOL multiply = [key isEqualToString:@"opacity"] || [key hasPrefix:@"scale"];
224
345
  double now = CACurrentMediaTime() * 1000;
225
- for (NSDictionary *layer in self.sampledLayers) {
226
- double part = inputValue(layer, self.layerAnchor, now);
227
- value = multiply ? value * part : value + part;
346
+ for (NSUInteger i = 0; i < _layerCount; i++) {
347
+ double part = sampledValue(&_layers[i], self.layerAnchor, now);
348
+ value = self.multiply ? value * part : value + part;
228
349
  }
229
- if ([key isEqualToString:@"opacity"]) { view.alpha = MAX(0, MIN(1, value)); return; }
230
- if ([key isEqualToString:@"x"]) key = @"transform.translation.x";
231
- else if ([key isEqualToString:@"y"]) key = @"transform.translation.y";
232
- else if ([key isEqualToString:@"scaleX"]) key = @"transform.scale.x";
233
- else if ([key isEqualToString:@"scaleY"]) key = @"transform.scale.y";
234
- else if ([key isEqualToString:@"rotate"]) { key = @"transform.rotation.z"; value *= M_PI / 180; }
235
- else if ([key isEqualToString:@"rotateX"]) { key = @"transform.rotation.x"; value *= M_PI / 180; }
236
- else if ([key isEqualToString:@"rotateY"]) { key = @"transform.rotation.y"; value *= M_PI / 180; }
237
- [view.layer setValue:@(value) forKeyPath:key];
238
- if ([view respondsToSelector:NSSelectorFromString(@"outerShadowContainerLayer")]) {
239
- CALayer *shadow = [view valueForKey:@"outerShadowContainerLayer"];
240
- [shadow setValue:@(value) forKeyPath:key];
350
+ if (self.alpha) { view.alpha = MAX(0, MIN(1, value)); return; }
351
+ if (self.radians) value *= M_PI / 180;
352
+ NSNumber *boxed = @(value);
353
+ [view.layer setValue:boxed forKeyPath:self.keyPath];
354
+ if (self.shadowed) {
355
+ CALayer *shadow = [(id<JoltShadowedView>)view outerShadowContainerLayer];
356
+ if (shadow) [shadow setValue:boxed forKeyPath:self.keyPath];
241
357
  }
242
358
  }
243
359
  @end
@@ -7,6 +7,7 @@ module JoltMotion {
7
7
  header "JoltPhotoZoom.h"
8
8
  header "JoltInteractiveAnimation.h"
9
9
  header "JoltInteractiveNavigation.h"
10
+ header "JoltInteractiveModal.h"
10
11
  header "JoltLiveSpring.h"
11
12
  header "JoltGestureSession.h"
12
13
  header "JoltReorderSession.h"
@@ -1,4 +1,13 @@
1
1
  declare namespace org.rafaelaugusto.jolt {
2
+ class KeyframeFactory {
3
+ static create(
4
+ view: android.view.View,
5
+ property: android.util.Property<android.view.View, java.lang.Float>,
6
+ times: readonly number[],
7
+ values: readonly number[],
8
+ unit: number,
9
+ ): android.animation.ObjectAnimator
10
+ }
2
11
  class ShapeLayer extends android.graphics.drawable.Drawable {
3
12
  constructor(options: string)
4
13
  static liveCount(): number
@@ -162,6 +171,7 @@ declare namespace org.rafaelaugusto.jolt {
162
171
  isMoving(): boolean
163
172
  follow(source: LiveSpring, mappings: string): void
164
173
  updateInput(snapshot: string): void
174
+ setInputs(rest: number, snapshots: string): void
165
175
  setTarget(value: number): void
166
176
  setReduced(value: boolean): void
167
177
  setPaused(value: boolean): void
package/src/animate.ts CHANGED
@@ -308,12 +308,30 @@ export function group(animations: readonly Animation[]): Animation {
308
308
  }
309
309
 
310
310
  export function toView(target: Target): View {
311
- if (target instanceof View) return target
312
- const view =
313
- (target as { nativeView?: unknown } | null)?.nativeView ??
314
- (target as { nativeElement?: unknown } | null)?.nativeElement
311
+ const view = unwrap(target)
315
312
  if (view instanceof View) return view
316
- throw new TypeError(`Expected a NativeScript view, got ${String(target)}.`)
313
+ throw new TypeError(`Expected a NativeScript view, got ${describe(target)}.`)
314
+ }
315
+
316
+ function unwrap(target: unknown, nested = false): unknown {
317
+ if (target instanceof View || target === null || typeof target !== 'object')
318
+ return target
319
+ const holder = target as {
320
+ nativeView?: unknown
321
+ nativeElement?: unknown
322
+ $el?: unknown
323
+ }
324
+ if (!nested && '$el' in holder) return unwrap(holder.$el, true)
325
+ return holder.nativeView ?? holder.nativeElement
326
+ }
327
+
328
+ function describe(target: unknown) {
329
+ if (target === null || typeof target !== 'object')
330
+ return typeof target === 'function' ? 'a function' : String(target)
331
+ if (Array.isArray(target)) return 'an array'
332
+ if ('$el' in target) return 'a component instance without a root view'
333
+ const name = Object.getPrototypeOf(target)?.constructor?.name
334
+ return name && name !== 'Object' ? `a ${name}` : 'an object without a view'
317
335
  }
318
336
 
319
337
  export function toViews(target: Target | readonly Target[]): View[] {
@@ -10,6 +10,8 @@ export interface SheetGeometry {
10
10
  height: number
11
11
  closedY: number
12
12
  top: number
13
+ /** How far the panel extends below the usable area, under a home indicator the keyboard does not cover. */
14
+ extension: number
13
15
  stops: SheetStop[]
14
16
  }
15
17
 
@@ -31,7 +33,10 @@ export function validateSnaps(points: readonly SheetSnap[]): void {
31
33
  throw new RangeError('Sheet snap points must not repeat.')
32
34
  }
33
35
 
34
- /** Keyboard overlap is relative to the host, so adjustResize is not subtracted twice. */
36
+ /**
37
+ * Keyboard overlap is relative to the host, so adjustResize is not subtracted twice.
38
+ * The panel extends under the bottom inset only when its host lets it reach that edge.
39
+ */
35
40
  export function sheetGeometry(
36
41
  points: readonly SheetSnap[],
37
42
  hostHeight: number,
@@ -41,6 +46,7 @@ export function sheetGeometry(
41
46
  safeTop: number,
42
47
  safeBottom: number,
43
48
  contentHeight: number,
49
+ extendable = true,
44
50
  ): SheetGeometry {
45
51
  const top = Math.max(0, safeTop)
46
52
  const overlap = Math.max(
@@ -48,6 +54,7 @@ export function sheetGeometry(
48
54
  Math.min(hostHeight, hostBottom - (windowHeight - keyboardHeight)),
49
55
  )
50
56
  const bottom = Math.max(Math.max(0, safeBottom), overlap)
57
+ const extension = extendable ? Math.max(0, Math.max(0, safeBottom) - overlap) : 0
51
58
  const height = Math.max(1, hostHeight - top - bottom)
52
59
  const closedY = Math.max(height, hostHeight - top)
53
60
  const stops = points.map((point) => {
@@ -60,7 +67,7 @@ export function sheetGeometry(
60
67
  const visible = Math.max(1, Math.min(height, desired))
61
68
  return { point, height: visible, y: height - visible }
62
69
  })
63
- return { height, closedY, top, stops }
70
+ return { height, closedY, top, extension, stops }
64
71
  }
65
72
 
66
73
  /** Projection selects a stop; the native spring retains the actual release velocity. */