@rafaelaugusto/jolt 0.1.0-beta.7 → 0.1.0-beta.9

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 (90) hide show
  1. package/README.md +12 -2
  2. package/dist/animate.d.ts +2 -0
  3. package/dist/animate.d.ts.map +1 -1
  4. package/dist/animate.js +7 -2
  5. package/dist/animate.js.map +1 -1
  6. package/dist/drag-input.d.ts +15 -0
  7. package/dist/drag-input.d.ts.map +1 -0
  8. package/dist/drag-input.js +66 -0
  9. package/dist/drag-input.js.map +1 -0
  10. package/dist/drag-physics.d.ts +20 -0
  11. package/dist/drag-physics.d.ts.map +1 -0
  12. package/dist/drag-physics.js +75 -0
  13. package/dist/drag-physics.js.map +1 -0
  14. package/dist/drag.d.ts +58 -0
  15. package/dist/drag.d.ts.map +1 -0
  16. package/dist/drag.js +324 -0
  17. package/dist/drag.js.map +1 -0
  18. package/dist/driver/android.d.ts.map +1 -1
  19. package/dist/driver/android.js +40 -0
  20. package/dist/driver/android.js.map +1 -1
  21. package/dist/driver/index.d.ts +10 -0
  22. package/dist/driver/index.d.ts.map +1 -1
  23. package/dist/driver/index.js.map +1 -1
  24. package/dist/driver/ios-layout.d.ts +9 -0
  25. package/dist/driver/ios-layout.d.ts.map +1 -0
  26. package/dist/driver/ios-layout.js +52 -0
  27. package/dist/driver/ios-layout.js.map +1 -0
  28. package/dist/driver/ios.d.ts.map +1 -1
  29. package/dist/driver/ios.js +85 -6
  30. package/dist/driver/ios.js.map +1 -1
  31. package/dist/engine.d.ts +6 -0
  32. package/dist/engine.d.ts.map +1 -1
  33. package/dist/engine.js +26 -0
  34. package/dist/engine.js.map +1 -1
  35. package/dist/index.d.ts +10 -0
  36. package/dist/index.d.ts.map +1 -1
  37. package/dist/index.js +5 -0
  38. package/dist/index.js.map +1 -1
  39. package/dist/interaction-surface.d.ts +12 -0
  40. package/dist/interaction-surface.d.ts.map +1 -0
  41. package/dist/interaction-surface.js +36 -0
  42. package/dist/interaction-surface.js.map +1 -0
  43. package/dist/layout.d.ts +13 -0
  44. package/dist/layout.d.ts.map +1 -0
  45. package/dist/layout.js +348 -0
  46. package/dist/layout.js.map +1 -0
  47. package/dist/motion-scope.d.ts +39 -0
  48. package/dist/motion-scope.d.ts.map +1 -0
  49. package/dist/motion-scope.js +238 -0
  50. package/dist/motion-scope.js.map +1 -0
  51. package/dist/motion-states.d.ts +36 -0
  52. package/dist/motion-states.d.ts.map +1 -0
  53. package/dist/motion-states.js +165 -0
  54. package/dist/motion-states.js.map +1 -0
  55. package/dist/press.d.ts.map +1 -1
  56. package/dist/press.js +14 -3
  57. package/dist/press.js.map +1 -1
  58. package/dist/track.d.ts.map +1 -1
  59. package/dist/track.js +21 -4
  60. package/dist/track.js.map +1 -1
  61. package/dist/transition.d.ts +2 -1
  62. package/dist/transition.d.ts.map +1 -1
  63. package/dist/transition.js.map +1 -1
  64. package/dist/vue/index.d.ts +3 -1
  65. package/dist/vue/index.d.ts.map +1 -1
  66. package/dist/vue/index.js +17 -4
  67. package/dist/vue/index.js.map +1 -1
  68. package/package.json +1 -1
  69. package/platforms/ios/src/JoltInteractiveAnimation.h +18 -0
  70. package/platforms/ios/src/JoltInteractiveAnimation.m +123 -0
  71. package/platforms/ios/src/module.modulemap +1 -0
  72. package/src/animate.ts +15 -8
  73. package/src/drag-input.ts +74 -0
  74. package/src/drag-physics.ts +110 -0
  75. package/src/drag.ts +395 -0
  76. package/src/driver/android.ts +38 -0
  77. package/src/driver/index.ts +11 -0
  78. package/src/driver/ios-layout.ts +63 -0
  79. package/src/driver/ios.ts +100 -8
  80. package/src/engine.ts +29 -0
  81. package/src/index.ts +17 -0
  82. package/src/interaction-surface.ts +41 -0
  83. package/src/ios-plugin.d.ts +22 -0
  84. package/src/layout.ts +408 -0
  85. package/src/motion-scope.ts +279 -0
  86. package/src/motion-states.ts +215 -0
  87. package/src/press.ts +15 -3
  88. package/src/track.ts +25 -4
  89. package/src/transition.ts +2 -0
  90. package/src/vue/index.ts +20 -3
@@ -0,0 +1,123 @@
1
+ #import "JoltInteractiveAnimation.h"
2
+ #import <QuartzCore/QuartzCore.h>
3
+
4
+ @interface JoltInteractiveAnimation ()
5
+ @property(nonatomic, weak) UIView *view;
6
+ @property(nonatomic, copy) NSArray<CALayer *> *layers;
7
+ @property(nonatomic, copy) NSString *keyPath;
8
+ @property(nonatomic, copy) NSArray<NSNumber *> *values;
9
+ @property(nonatomic, copy) NSArray<NSNumber *> *times;
10
+ @property(nonatomic) double duration, delay, repeats, anchor, offset, finalValue;
11
+ @property(nonatomic) BOOL reverse, paused, stopped;
12
+ @property(nonatomic, copy, nullable) void (^completion)(BOOL);
13
+ @end
14
+
15
+ @implementation JoltInteractiveAnimation
16
+ static NSMapTable<UIView *, NSMutableDictionary<NSString *, JoltInteractiveAnimation *> *> *views;
17
+ static NSMutableSet<JoltInteractiveAnimation *> *playing;
18
+ static CADisplayLink *displayLink;
19
+
20
+ + (void)initialize {
21
+ if (self != [JoltInteractiveAnimation class]) return;
22
+ views = [NSMapTable weakToStrongObjectsMapTable];
23
+ playing = [NSMutableSet new];
24
+ }
25
+ + (void)schedule {
26
+ if (playing.count && !displayLink) {
27
+ displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(tick:)];
28
+ if (@available(iOS 15.0, *)) {
29
+ float maximum = MIN(120, UIScreen.mainScreen.maximumFramesPerSecond);
30
+ displayLink.preferredFrameRateRange = CAFrameRateRangeMake(MIN(80, maximum), maximum, maximum);
31
+ }
32
+ [displayLink addToRunLoop:NSRunLoop.mainRunLoop forMode:NSRunLoopCommonModes];
33
+ } else if (!playing.count && displayLink) {
34
+ [displayLink invalidate]; displayLink = nil;
35
+ }
36
+ }
37
+ + (void)tick:(CADisplayLink *)link {
38
+ for (JoltInteractiveAnimation *track in playing.allObjects) {
39
+ if (track.stopped || track.paused) continue;
40
+ if (!track.view) { [track finish:NO]; continue; }
41
+ double elapsed = track.elapsed;
42
+ [track apply:elapsed];
43
+ if (isfinite(track.repeats) && elapsed >= track.delay + track.duration * (track.repeats + 1))
44
+ [track finish:YES];
45
+ }
46
+ }
47
+ + (instancetype)startView:(UIView *)view layers:(NSArray<CALayer *> *)layers keyPath:(NSString *)keyPath
48
+ values:(NSArray<NSNumber *> *)values times:(NSArray<NSNumber *> *)times
49
+ duration:(double)duration delay:(double)delay repeat:(double)repeat
50
+ reverse:(BOOL)reverse finalValue:(double)finalValue
51
+ startedAt:(double)startedAt completion:(void (^)(BOOL))completion {
52
+ JoltInteractiveAnimation *track = [self new];
53
+ track.view = view; track.layers = layers; track.keyPath = keyPath; track.values = values; track.times = times;
54
+ track.duration = MAX(1, duration); track.delay = delay; track.repeats = repeat;
55
+ track.reverse = reverse; track.finalValue = finalValue;
56
+ track.anchor = startedAt; track.completion = completion;
57
+ NSMutableDictionary *properties = [views objectForKey:view];
58
+ if (!properties) { properties = [NSMutableDictionary new]; [views setObject:properties forKey:view]; }
59
+ JoltInteractiveAnimation *previous = properties[keyPath];
60
+ [previous cancel];
61
+ // Cancelling the last property can remove the dictionary from the weak map.
62
+ [views setObject:properties forKey:view];
63
+ properties[keyPath] = track;
64
+ [track apply:track.elapsed];
65
+ [playing addObject:track]; [self schedule];
66
+ return track;
67
+ }
68
+ - (double)elapsed { return self.offset + (self.paused ? 0 : CACurrentMediaTime() * 1000 - self.anchor); }
69
+ - (double)value:(double)elapsed {
70
+ double total = self.delay + self.duration * (self.repeats + 1);
71
+ if (isfinite(total) && elapsed >= total) return self.finalValue;
72
+ double time = MAX(0, elapsed - self.delay);
73
+ double iteration = floor(time / self.duration);
74
+ double progress = fmod(time, self.duration) / self.duration;
75
+ if (self.reverse && fmod(iteration, 2) >= 1) progress = 1 - progress;
76
+ NSUInteger low = 0, high = self.times.count - 1;
77
+ while (low + 1 < high) {
78
+ NSUInteger middle = (low + high) / 2;
79
+ if (self.times[middle].doubleValue <= progress) low = middle; else high = middle;
80
+ }
81
+ double start = self.times[low].doubleValue, end = self.times[high].doubleValue;
82
+ double fraction = end > start ? MIN(1, MAX(0, (progress - start) / (end - start))) : 1;
83
+ return self.values[low].doubleValue + (self.values[high].doubleValue - self.values[low].doubleValue) * fraction;
84
+ }
85
+ - (void)apply:(double)elapsed {
86
+ UIView *view = self.view;
87
+ if (!view || self.stopped) return;
88
+ double value = [self value:elapsed];
89
+ [CATransaction begin]; [CATransaction setDisableActions:YES];
90
+ if ([self.keyPath isEqualToString:@"opacity"]) view.alpha = value;
91
+ for (CALayer *layer in self.layers) [layer setValue:@(value) forKeyPath:self.keyPath];
92
+ [CATransaction commit];
93
+ }
94
+ - (void)control:(double)elapsed paused:(BOOL)paused {
95
+ if (self.stopped) return;
96
+ self.offset = elapsed; self.anchor = CACurrentMediaTime() * 1000; self.paused = paused;
97
+ [self apply:elapsed];
98
+ if (paused) [playing removeObject:self]; else [playing addObject:self];
99
+ [[self class] schedule];
100
+ }
101
+ - (void)finish:(BOOL)completed {
102
+ if (self.stopped) return;
103
+ self.stopped = YES;
104
+ [playing removeObject:self];
105
+ UIView *view = self.view;
106
+ NSMutableDictionary *properties = view ? [views objectForKey:view] : nil;
107
+ if (properties[self.keyPath] == self) [properties removeObjectForKey:self.keyPath];
108
+ if (view && !properties.count) [views removeObjectForKey:view];
109
+ void (^callback)(BOOL) = self.completion; self.completion = nil;
110
+ [[self class] schedule];
111
+ if (callback) callback(completed);
112
+ }
113
+ - (void)cancel { [self finish:NO]; }
114
+ + (void)syncView:(UIView *)view excluding:(NSString *)keyPath {
115
+ for (JoltInteractiveAnimation *track in [views objectForKey:view].allValues)
116
+ if (![track.keyPath isEqualToString:keyPath]) [track apply:track.elapsed];
117
+ }
118
+ + (NSNumber *)valueForView:(UIView *)view keyPath:(NSString *)keyPath {
119
+ JoltInteractiveAnimation *track = [views objectForKey:view][keyPath];
120
+ return track ? [view.layer valueForKeyPath:keyPath] : nil;
121
+ }
122
+ + (NSUInteger)activeCount { return playing.count; }
123
+ @end
@@ -1,5 +1,6 @@
1
1
  module JoltMotion {
2
2
  header "JoltImageEffect.h"
3
3
  header "JoltPhotoZoom.h"
4
+ header "JoltInteractiveAnimation.h"
4
5
  export *
5
6
  }
package/src/animate.ts CHANGED
@@ -67,6 +67,15 @@ export function animate(
67
67
  animation: Keyframes | PresetName,
68
68
  options?: AnimateOptions | SpringName,
69
69
  ): PlaybackAnimation {
70
+ return prepareAnimation(target, animation, options)()
71
+ }
72
+
73
+ /** Validate and snapshot a complete animation before changing any view ownership. */
74
+ export function prepareAnimation(
75
+ target: Target | readonly Target[],
76
+ animation: Keyframes | PresetName,
77
+ options?: AnimateOptions | SpringName,
78
+ ): () => PlaybackAnimation {
70
79
  const { keyframes, base, overrides, stagger, relative, haptic, path } = prepare(
71
80
  animation,
72
81
  options,
@@ -78,11 +87,8 @@ export function animate(
78
87
  for (const property of Object.keys(keyframes))
79
88
  if (isColorProperty(property)) validateColorTarget(view, property)
80
89
  else if (isDecorationProperty(property)) validateDecorationTarget(view, property)
81
- return new ViewAnimation(
82
- view,
83
- plan(keyframes, base, overrides, 0, relative, path),
84
- feedback,
85
- )
90
+ const plans = plan(keyframes, base, overrides, 0, relative, path)
91
+ return () => new ViewAnimation(view, plans, feedback)
86
92
  }
87
93
 
88
94
  const views = target.map(toView)
@@ -100,9 +106,10 @@ export function animate(
100
106
  path,
101
107
  ),
102
108
  )
103
- return playbackGroup(
104
- views.map((view, index) => new ViewAnimation(view, plans[index]!, feedback, true)),
105
- )
109
+ return () =>
110
+ playbackGroup(
111
+ views.map((view, index) => new ViewAnimation(view, plans[index]!, feedback, true)),
112
+ )
106
113
  }
107
114
 
108
115
  /** Starts at the current translation and slows down from a velocity in DIPs per second. */
@@ -0,0 +1,74 @@
1
+ import { Utils, type PanGestureEventData, type View } from '@nativescript/core'
2
+ import { getDriver } from './driver/index.js'
3
+
4
+ interface Sample {
5
+ x: number
6
+ y: number
7
+ time: number
8
+ }
9
+
10
+ /** Android's raw coordinates avoid feedback from translating the touched view. */
11
+ export class DragInput {
12
+ private samples: Sample[] = []
13
+ private lastMoved = 0
14
+ reset() {
15
+ this.samples = []
16
+ this.lastMoved = 0
17
+ }
18
+ read(event: PanGestureEventData, view: View) {
19
+ const ios = event.ios as UIPanGestureRecognizer | undefined
20
+ const android = event.android?.current as android.view.MotionEvent | undefined
21
+ const parent = view.parent?.nativeViewProtected ?? view.nativeViewProtected
22
+ const delta = ios
23
+ ? ios.translationInView(parent)
24
+ : { x: event.deltaX, y: event.deltaY }
25
+ const density = Utils.layout.getDisplayDensity()
26
+ const sample: Sample = android
27
+ ? {
28
+ x: android.getRawX() / density,
29
+ y: android.getRawY() / density,
30
+ time: Number(android.getEventTime()),
31
+ }
32
+ : { ...delta, time: getDriver().now() }
33
+ if (
34
+ Number.isFinite(sample.x) &&
35
+ Number.isFinite(sample.y) &&
36
+ Number.isFinite(sample.time)
37
+ ) {
38
+ const previous = this.samples.at(-1)
39
+ if (!previous || sample.x !== previous.x || sample.y !== previous.y)
40
+ this.lastMoved = sample.time
41
+ // Replace duplicate times (Android sends began + changed for the same event).
42
+ if (this.samples.at(-1)?.time === sample.time) this.samples.pop()
43
+ this.samples.push(sample)
44
+ while (this.samples.length > 2 && this.samples[1]!.time < sample.time - 100)
45
+ this.samples.shift()
46
+ }
47
+ const first = this.samples[0]
48
+ const elapsed = first ? sample.time - first.time : 0
49
+ // UIKit may retain its last movement velocity when the finger holds still
50
+ // before lifting. A stale velocity must not turn that hold into a fling.
51
+ const nativeVelocity = ios
52
+ ? sample.time - this.lastMoved > 100
53
+ ? { x: 0, y: 0 }
54
+ : ios.velocityInView(parent)
55
+ : undefined
56
+ const velocity = nativeVelocity ?? {
57
+ x: elapsed > 0 && elapsed <= 150 ? ((sample.x - first!.x) * 1000) / elapsed : 0,
58
+ y: elapsed > 0 && elapsed <= 150 ? ((sample.y - first!.y) * 1000) / elapsed : 0,
59
+ }
60
+ const action = android?.getActionMasked()
61
+ return {
62
+ x: delta.x,
63
+ y: delta.y,
64
+ velocityX: Number.isFinite(velocity.x) ? velocity.x : 0,
65
+ velocityY: Number.isFinite(velocity.y) ? velocity.y : 0,
66
+ cancelled:
67
+ event.state === 0 ||
68
+ action === 3 ||
69
+ action === 5 ||
70
+ action === 6 ||
71
+ (android ? android.getPointerCount() > 1 : (ios?.numberOfTouches ?? 1) > 1),
72
+ }
73
+ }
74
+ }
@@ -0,0 +1,110 @@
1
+ import { solveDecay } from './decay.js'
2
+ import { solveSpring, springPhysics } from './spring.js'
3
+
4
+ export interface DragCurve {
5
+ type: 'drag'
6
+ velocity: number
7
+ friction: number
8
+ bounds: readonly [number, number]
9
+ snap?: readonly number[]
10
+ rubberband: boolean
11
+ }
12
+
13
+ export const clampDrag = (value: number, [min, max]: readonly [number, number]) =>
14
+ Math.max(min, Math.min(max, value))
15
+
16
+ /** Bounded resistance; coordinates and the resistance length are DIPs. */
17
+ export function resistDrag(
18
+ value: number,
19
+ bounds: readonly [number, number],
20
+ length: number,
21
+ ) {
22
+ const edge = clampDrag(value, bounds)
23
+ const distance = value - edge
24
+ return edge + (distance / (1 + Math.abs(distance) / length)) * 0.55
25
+ }
26
+
27
+ /** Recover the finger-space origin when grabbing an already rubberbanded pose. */
28
+ export function resumeDrag(
29
+ value: number,
30
+ bounds: readonly [number, number],
31
+ length: number,
32
+ ) {
33
+ const edge = clampDrag(value, bounds)
34
+ const distance = value - edge
35
+ // A native edge spring or external owner can exceed the usual resistance range.
36
+ // Expand it for this gesture so every finite visible pose has a stable inverse.
37
+ length = Math.max(length, Math.abs(distance) * 4)
38
+ return { origin: edge + distance / (0.55 - Math.abs(distance) / length), length }
39
+ }
40
+
41
+ export function dragDestination(from: number, curve: DragCurve) {
42
+ const projected = solveDecay(from, curve.velocity, curve.friction, 0.25).to
43
+ if (curve.snap)
44
+ return curve.snap.reduce((best, point) =>
45
+ Math.abs(point - projected) < Math.abs(best - projected) ? point : best,
46
+ )
47
+ return clampDrag(projected, curve.bounds)
48
+ }
49
+
50
+ /** One precomputed motion. Native keyframes play both the coast and edge recovery. */
51
+ export function solveDragRelease(from: number, curve: DragCurve) {
52
+ const to = dragDestination(from, curve)
53
+ const coast = solveDecay(from, curve.velocity, curve.friction, 0.25)
54
+ const edge = clampDrag(from, curve.bounds)
55
+ const springTo = (start: number, target: number, velocity: number) => {
56
+ const motion = solveSpring(
57
+ springPhysics({ duration: 500, bounce: 0 }),
58
+ start,
59
+ target,
60
+ velocity,
61
+ 0.25,
62
+ )
63
+ const position = (time: number) => {
64
+ const value = time >= motion.duration ? target : motion.position(time)
65
+ return curve.rubberband ? value : clampDrag(value, curve.bounds)
66
+ }
67
+ return {
68
+ ...motion,
69
+ position,
70
+ velocity: (time: number) => {
71
+ const raw = motion.position(time)
72
+ return time >= motion.duration ||
73
+ (!curve.rubberband && raw !== clampDrag(raw, curve.bounds))
74
+ ? 0
75
+ : motion.velocity(time)
76
+ },
77
+ }
78
+ }
79
+ if (curve.snap || from !== edge)
80
+ return {
81
+ ...springTo(from, curve.snap ? to : edge, curve.velocity),
82
+ to: curve.snap ? to : edge,
83
+ }
84
+ if (to === coast.to) return coast
85
+
86
+ // Reach the boundary with the decay's actual velocity, then absorb that velocity.
87
+ const rate = -Math.log(curve.friction)
88
+ const collision = Math.max(
89
+ 0,
90
+ -Math.log1p((-(to - from) * rate * 1000) / curve.velocity) / rate,
91
+ )
92
+ const recovery = springTo(to, to, coast.velocity(collision))
93
+ const duration = collision + (curve.rubberband ? recovery.duration : 0)
94
+ return {
95
+ to,
96
+ duration,
97
+ position: (time: number) =>
98
+ time >= duration
99
+ ? to
100
+ : time < collision
101
+ ? coast.position(time)
102
+ : recovery.position(time - collision),
103
+ velocity: (time: number) =>
104
+ time >= duration
105
+ ? 0
106
+ : time < collision
107
+ ? coast.velocity(time)
108
+ : recovery.velocity(time - collision),
109
+ }
110
+ }