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

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 (184) hide show
  1. package/README.md +122 -5
  2. package/dist/animate.d.ts +4 -2
  3. package/dist/animate.d.ts.map +1 -1
  4. package/dist/animate.js +48 -11
  5. package/dist/animate.js.map +1 -1
  6. package/dist/config.d.ts +22 -0
  7. package/dist/config.d.ts.map +1 -0
  8. package/dist/config.js +34 -0
  9. package/dist/config.js.map +1 -0
  10. package/dist/driver/android.d.ts.map +1 -1
  11. package/dist/driver/android.js +7 -0
  12. package/dist/driver/android.js.map +1 -1
  13. package/dist/driver/index.d.ts +2 -0
  14. package/dist/driver/index.d.ts.map +1 -1
  15. package/dist/driver/index.js.map +1 -1
  16. package/dist/driver/ios-color.js +1 -1
  17. package/dist/driver/ios-color.js.map +1 -1
  18. package/dist/driver/ios-decoration.js +1 -1
  19. package/dist/driver/ios-decoration.js.map +1 -1
  20. package/dist/driver/ios-playback.d.ts +1 -1
  21. package/dist/driver/ios-playback.d.ts.map +1 -1
  22. package/dist/driver/ios-playback.js +14 -0
  23. package/dist/driver/ios-playback.js.map +1 -1
  24. package/dist/driver/ios.d.ts.map +1 -1
  25. package/dist/driver/ios.js +1 -0
  26. package/dist/driver/ios.js.map +1 -1
  27. package/dist/engine.d.ts +16 -1
  28. package/dist/engine.d.ts.map +1 -1
  29. package/dist/engine.js +33 -1
  30. package/dist/engine.js.map +1 -1
  31. package/dist/image-effect-native.d.ts +26 -0
  32. package/dist/image-effect-native.d.ts.map +1 -0
  33. package/dist/image-effect-native.js +73 -0
  34. package/dist/image-effect-native.js.map +1 -0
  35. package/dist/image-effect.d.ts +50 -0
  36. package/dist/image-effect.d.ts.map +1 -0
  37. package/dist/image-effect.js +290 -0
  38. package/dist/image-effect.js.map +1 -0
  39. package/dist/image-owner.d.ts +3 -0
  40. package/dist/image-owner.d.ts.map +1 -0
  41. package/dist/image-owner.js +11 -0
  42. package/dist/image-owner.js.map +1 -0
  43. package/dist/index.d.ts +15 -1
  44. package/dist/index.d.ts.map +1 -1
  45. package/dist/index.js +7 -0
  46. package/dist/index.js.map +1 -1
  47. package/dist/items.d.ts +20 -0
  48. package/dist/items.d.ts.map +1 -0
  49. package/dist/items.js +109 -0
  50. package/dist/items.js.map +1 -0
  51. package/dist/keyboard.d.ts +17 -0
  52. package/dist/keyboard.d.ts.map +1 -0
  53. package/dist/keyboard.js +116 -0
  54. package/dist/keyboard.js.map +1 -0
  55. package/dist/label.d.ts +7 -0
  56. package/dist/label.d.ts.map +1 -0
  57. package/dist/label.js +60 -0
  58. package/dist/label.js.map +1 -0
  59. package/dist/liquid.d.ts.map +1 -1
  60. package/dist/liquid.js +6 -1
  61. package/dist/liquid.js.map +1 -1
  62. package/dist/loader.d.ts.map +1 -1
  63. package/dist/loader.js +2 -1
  64. package/dist/loader.js.map +1 -1
  65. package/dist/particles.d.ts.map +1 -1
  66. package/dist/particles.js +4 -3
  67. package/dist/particles.js.map +1 -1
  68. package/dist/path.d.ts +12 -0
  69. package/dist/path.d.ts.map +1 -0
  70. package/dist/path.js +90 -0
  71. package/dist/path.js.map +1 -0
  72. package/dist/playback-group.d.ts.map +1 -1
  73. package/dist/playback-group.js +29 -1
  74. package/dist/playback-group.js.map +1 -1
  75. package/dist/press.d.ts.map +1 -1
  76. package/dist/press.js +3 -3
  77. package/dist/press.js.map +1 -1
  78. package/dist/reveal.d.ts.map +1 -1
  79. package/dist/reveal.js +3 -2
  80. package/dist/reveal.js.map +1 -1
  81. package/dist/roll-diff.d.ts +22 -0
  82. package/dist/roll-diff.d.ts.map +1 -0
  83. package/dist/roll-diff.js +36 -0
  84. package/dist/roll-diff.js.map +1 -0
  85. package/dist/roll.d.ts +29 -0
  86. package/dist/roll.d.ts.map +1 -0
  87. package/dist/roll.js +442 -0
  88. package/dist/roll.js.map +1 -0
  89. package/dist/scroll-linked.d.ts +10 -0
  90. package/dist/scroll-linked.d.ts.map +1 -0
  91. package/dist/scroll-linked.js +58 -0
  92. package/dist/scroll-linked.js.map +1 -0
  93. package/dist/sequence.d.ts +10 -5
  94. package/dist/sequence.d.ts.map +1 -1
  95. package/dist/sequence.js +202 -14
  96. package/dist/sequence.js.map +1 -1
  97. package/dist/shimmer.d.ts.map +1 -1
  98. package/dist/shimmer.js +2 -1
  99. package/dist/shimmer.js.map +1 -1
  100. package/dist/svg-path.d.ts +14 -0
  101. package/dist/svg-path.d.ts.map +1 -0
  102. package/dist/svg-path.js +281 -0
  103. package/dist/svg-path.js.map +1 -0
  104. package/dist/symbol-effect.d.ts.map +1 -1
  105. package/dist/symbol-effect.js +4 -3
  106. package/dist/symbol-effect.js.map +1 -1
  107. package/dist/text-layout.d.ts +8 -0
  108. package/dist/text-layout.d.ts.map +1 -1
  109. package/dist/text-layout.js +8 -2
  110. package/dist/text-layout.js.map +1 -1
  111. package/dist/text.d.ts +1 -1
  112. package/dist/text.d.ts.map +1 -1
  113. package/dist/text.js +60 -64
  114. package/dist/text.js.map +1 -1
  115. package/dist/track.d.ts +2 -0
  116. package/dist/track.d.ts.map +1 -1
  117. package/dist/track.js +62 -15
  118. package/dist/track.js.map +1 -1
  119. package/dist/types.d.ts +7 -1
  120. package/dist/types.d.ts.map +1 -1
  121. package/dist/vue/index.d.ts +89 -0
  122. package/dist/vue/index.d.ts.map +1 -0
  123. package/dist/vue/index.js +134 -0
  124. package/dist/vue/index.js.map +1 -0
  125. package/dist/zoom-native.d.ts +30 -0
  126. package/dist/zoom-native.d.ts.map +1 -0
  127. package/dist/zoom-native.js +56 -0
  128. package/dist/zoom-native.js.map +1 -0
  129. package/dist/zoomable.d.ts +48 -0
  130. package/dist/zoomable.d.ts.map +1 -0
  131. package/dist/zoomable.js +249 -0
  132. package/dist/zoomable.js.map +1 -0
  133. package/package.json +16 -3
  134. package/platforms/android/java/org/rafaelaugusto/jolt/ImageEffect.java +288 -0
  135. package/platforms/android/java/org/rafaelaugusto/jolt/KeyboardFollower.java +91 -0
  136. package/platforms/android/java/org/rafaelaugusto/jolt/NumberRoll.java +190 -0
  137. package/platforms/android/java/org/rafaelaugusto/jolt/PhotoZoom.java +250 -0
  138. package/platforms/android/java/org/rafaelaugusto/jolt/TextReveal.java +180 -0
  139. package/platforms/android/proguard-rules.pro +10 -0
  140. package/platforms/ios/build.xcconfig +1 -0
  141. package/platforms/ios/src/JoltImageEffect.h +25 -0
  142. package/platforms/ios/src/JoltImageEffect.mm +242 -0
  143. package/platforms/ios/src/JoltPhotoZoom.h +23 -0
  144. package/platforms/ios/src/JoltPhotoZoom.m +229 -0
  145. package/platforms/ios/src/module.modulemap +5 -0
  146. package/src/android-plugin.d.ts +142 -0
  147. package/src/animate.ts +80 -30
  148. package/src/config.ts +46 -0
  149. package/src/driver/android.ts +14 -0
  150. package/src/driver/index.ts +2 -0
  151. package/src/driver/ios-color.ts +1 -1
  152. package/src/driver/ios-decoration.ts +1 -1
  153. package/src/driver/ios-playback.ts +15 -1
  154. package/src/driver/ios.ts +2 -0
  155. package/src/engine.ts +47 -2
  156. package/src/image-effect-native.ts +129 -0
  157. package/src/image-effect.ts +336 -0
  158. package/src/image-owner.ts +12 -0
  159. package/src/index.ts +20 -1
  160. package/src/ios-plugin.d.ts +59 -0
  161. package/src/items.ts +140 -0
  162. package/src/keyboard.ts +132 -0
  163. package/src/label.ts +76 -0
  164. package/src/liquid.ts +6 -1
  165. package/src/loader.ts +2 -1
  166. package/src/particles.ts +4 -4
  167. package/src/path.ts +124 -0
  168. package/src/playback-group.ts +34 -1
  169. package/src/press.ts +3 -5
  170. package/src/reveal.ts +3 -3
  171. package/src/roll-diff.ts +57 -0
  172. package/src/roll.ts +515 -0
  173. package/src/scroll-linked.ts +73 -0
  174. package/src/sequence.ts +199 -14
  175. package/src/shimmer.ts +2 -1
  176. package/src/svg-path.ts +355 -0
  177. package/src/symbol-effect.ts +4 -3
  178. package/src/text-layout.ts +14 -2
  179. package/src/text.ts +62 -81
  180. package/src/track.ts +89 -24
  181. package/src/types.ts +8 -1
  182. package/src/vue/index.ts +189 -0
  183. package/src/zoom-native.ts +124 -0
  184. package/src/zoomable.ts +330 -0
@@ -0,0 +1,229 @@
1
+ #import "JoltPhotoZoom.h"
2
+ #import <QuartzCore/QuartzCore.h>
3
+
4
+ static double JoltClamp(double v,double lo,double hi){return MAX(lo,MIN(hi,v));}
5
+ static double JoltRubber(double v,double lo,double hi,double size){
6
+ double edge=JoltClamp(v,lo,hi),d=v-edge;
7
+ return edge+copysign(size*(1-1/(fabs(d)*.55/MAX(size,1)+1)),d);
8
+ }
9
+
10
+ @interface JoltPhotoZoom () <UIGestureRecognizerDelegate>
11
+ @property(nonatomic,weak) UIImageView *image;
12
+ @property(nonatomic,weak) UIView *viewport,*backdrop,*chrome;
13
+ @property(nonatomic,strong) UIPinchGestureRecognizer *pinch;
14
+ @property(nonatomic,strong) UIPanGestureRecognizer *pan;
15
+ @property(nonatomic,strong) UITapGestureRecognizer *tap;
16
+ @property(nonatomic,strong) CADisplayLink *clock;
17
+ @property(nonatomic,copy) void (^onDismiss)(double,double,double,double,double);
18
+ @property(nonatomic,copy) void (^onSettled)(double,double,double);
19
+ @property(nonatomic) CGAffineTransform original;
20
+ @property(nonatomic) BOOL originalInteraction,originalClip,reduced,active,disposed,dismiss,closing;
21
+ @property(nonatomic) double minimum,maximum,doubleTap,backdropAlpha,chromeAlpha;
22
+ @property(nonatomic,readwrite) double x,y,scale,dismissProgress;
23
+ @property(nonatomic,readwrite) BOOL dismissPending;
24
+ @property(nonatomic) CGPoint anchor,lastFocus,lastPan,initialTranslation;
25
+ @property(nonatomic) CGPoint rawPan;
26
+ @property(nonatomic) double pinchFloor;
27
+ @property(nonatomic) CGRect content;
28
+ @property(nonatomic) CGSize viewportSize,imageSize;
29
+ @property(nonatomic) NSInteger mode,touches;
30
+ @property(nonatomic) double vx,vy,startTime,startX,startY,startScale,startProgress,targetX,targetY,targetScale;
31
+ @end
32
+
33
+ @implementation JoltPhotoZoom
34
+ + (instancetype)attachImage:(UIImageView *)image viewport:(UIView *)viewport
35
+ minimum:(double)minimum maximum:(double)maximum doubleTap:(double)doubleTap
36
+ dismiss:(BOOL)dismiss reduced:(BOOL)reduced backdrop:(UIView *)backdrop chrome:(UIView *)chrome
37
+ onDismiss:(void (^)(double,double,double,double,double))onDismiss
38
+ onSettled:(void (^)(double,double,double))onSettled {
39
+ JoltPhotoZoom *z=[JoltPhotoZoom new];
40
+ z.image=image;z.viewport=viewport;z.backdrop=backdrop;z.chrome=chrome;
41
+ z.minimum=minimum;z.maximum=maximum;z.doubleTap=doubleTap;z.dismiss=dismiss;z->_reduced=reduced;z.active=YES;
42
+ z.onDismiss=onDismiss;z.onSettled=onSettled;
43
+ z.original=image.transform;z.originalInteraction=viewport.userInteractionEnabled;z.originalClip=viewport.clipsToBounds;
44
+ z.backdropAlpha=backdrop?backdrop.alpha:1;z.chromeAlpha=chrome?chrome.alpha:1;z.scale=minimum;
45
+ z.pan=[[UIPanGestureRecognizer alloc] initWithTarget:z action:@selector(panned:)];
46
+ z.pinch=[[UIPinchGestureRecognizer alloc] initWithTarget:z action:@selector(pinched:)];
47
+ z.tap=[[UITapGestureRecognizer alloc] initWithTarget:z action:@selector(tapped:)];
48
+ z.tap.numberOfTapsRequired=2;z.pan.delegate=z;z.pinch.delegate=z;z.tap.delegate=z;
49
+ [viewport addGestureRecognizer:z.pan];[viewport addGestureRecognizer:z.pinch];[viewport addGestureRecognizer:z.tap];
50
+ viewport.userInteractionEnabled=YES;viewport.clipsToBounds=YES;
51
+ [z measure];[z apply];
52
+ return z;
53
+ }
54
+ - (void)measure {
55
+ self.viewportSize=self.viewport.bounds.size;self.imageSize=self.image.bounds.size;
56
+ CGSize intrinsic=self.image.image.size;
57
+ double iw=self.imageSize.width,ih=self.imageSize.height,dw=intrinsic.width,dh=intrinsic.height;
58
+ if(dw<=0 || dh<=0){dw=iw;dh=ih;}
59
+ UIViewContentMode fit=self.image.contentMode;
60
+ double factor=fit==UIViewContentModeScaleAspectFill?MAX(iw/dw,ih/dh):MIN(iw/dw,ih/dh);
61
+ if(fit==UIViewContentModeScaleToFill){dw=iw;dh=ih;}
62
+ else if(fit!=UIViewContentModeCenter && fit!=UIViewContentModeTopLeft){dw*=factor;dh*=factor;}
63
+ CGPoint pivot=CGPointMake(self.image.layer.anchorPoint.x*iw,self.image.layer.anchorPoint.y*ih);
64
+ self.anchor=CGPointMake(self.image.layer.position.x+self.original.tx,self.image.layer.position.y+self.original.ty);
65
+ double ox=fit==UIViewContentModeTopLeft?0:(iw-dw)/2,oy=fit==UIViewContentModeTopLeft?0:(ih-dh)/2;
66
+ double left=MAX(0,ox),top=MAX(0,oy),right=MIN(iw,ox+dw),bottom=MIN(ih,oy+dh);
67
+ self.content=CGRectMake((left-pivot.x)*self.original.a,(top-pivot.y)*self.original.d,(right-left)*self.original.a,(bottom-top)*self.original.d);
68
+ }
69
+ - (CGRect)boundsFor:(double)scale {
70
+ double loX=self.viewportSize.width-self.anchor.x-CGRectGetMaxX(self.content)*scale;
71
+ double hiX=-self.anchor.x-CGRectGetMinX(self.content)*scale;
72
+ double loY=self.viewportSize.height-self.anchor.y-CGRectGetMaxY(self.content)*scale;
73
+ double hiY=-self.anchor.y-CGRectGetMinY(self.content)*scale;
74
+ if(loX>hiX)loX=hiX=(loX+hiX)/2;
75
+ if(loY>hiY)loY=hiY=(loY+hiY)/2;
76
+ return CGRectMake(loX,loY,hiX-loX,hiY-loY);
77
+ }
78
+ - (void)apply {
79
+ self.image.transform=CGAffineTransformMake(self.original.a*self.scale,0,0,self.original.d*self.scale,self.original.tx+self.x,self.original.ty+self.y);
80
+ self.backdrop.alpha=self.backdropAlpha*(1-self.dismissProgress);
81
+ self.chrome.alpha=self.chromeAlpha*(1-self.dismissProgress);
82
+ }
83
+ - (void)zoom:(double)value focusX:(double)fx focusY:(double)fy animated:(BOOL)animated {
84
+ if(self.disposed || self.dismissPending)return;
85
+ [self cancelAnimation];double next=JoltClamp(value,self.minimum,self.maximum);
86
+ double x=fx-self.anchor.x-(fx-self.anchor.x-self.x)*next/self.scale;
87
+ double y=fy-self.anchor.y-(fy-self.anchor.y-self.y)*next/self.scale;
88
+ CGRect b=[self boundsFor:next];
89
+ [self springX:JoltClamp(x,CGRectGetMinX(b),CGRectGetMaxX(b)) y:JoltClamp(y,CGRectGetMinY(b),CGRectGetMaxY(b)) scale:next vx:0 vy:0 animated:animated close:NO];
90
+ }
91
+ - (void)reset:(BOOL)animated {
92
+ if(self.disposed)return;
93
+ self.dismissPending=NO;self.mode=0;CGRect b=[self boundsFor:self.minimum];
94
+ [self springX:JoltClamp(0,CGRectGetMinX(b),CGRectGetMaxX(b)) y:JoltClamp(0,CGRectGetMinY(b),CGRectGetMaxY(b)) scale:self.minimum vx:0 vy:0 animated:animated close:NO];
95
+ }
96
+ - (void)requestDismiss {
97
+ if(self.disposed || self.dismissPending || !self.dismiss)return;
98
+ CGRect b=[self boundsFor:self.minimum];
99
+ [self springX:JoltClamp(0,CGRectGetMinX(b),CGRectGetMaxX(b)) y:JoltClamp(0,CGRectGetMinY(b),CGRectGetMaxY(b)) scale:self.minimum vx:0 vy:0 animated:YES close:YES];
100
+ }
101
+ - (void)setReduced:(BOOL)reduced { _reduced=reduced;if(reduced){if(self.dismissPending)[self reset:NO];else [self settle:NO];} }
102
+ - (void)setActive:(BOOL)active {
103
+ _active=active;self.pan.enabled=active;self.pinch.enabled=active;self.tap.enabled=active;
104
+ if(!active){[self cancelAnimation];self.dismissPending=NO;[self reset:NO];}
105
+ }
106
+ - (void)layout {
107
+ if(self.disposed)return;
108
+ if(CGSizeEqualToSize(self.viewportSize,self.viewport.bounds.size) && CGSizeEqualToSize(self.imageSize,self.image.bounds.size))return;
109
+ [self cancelAnimation];self.dismissPending=NO;[self measure];self.vx=0;self.vy=0;self.mode=0;[self settle:NO];
110
+ }
111
+ - (void)stop {
112
+ if(self.disposed)return;
113
+ self.disposed=YES;[self cancelAnimation];
114
+ [self.viewport removeGestureRecognizer:self.pan];[self.viewport removeGestureRecognizer:self.pinch];[self.viewport removeGestureRecognizer:self.tap];
115
+ self.viewport.userInteractionEnabled=self.originalInteraction;self.viewport.clipsToBounds=self.originalClip;
116
+ self.image.transform=self.original;self.backdrop.alpha=self.backdropAlpha;self.chrome.alpha=self.chromeAlpha;
117
+ self.onDismiss=nil;self.onSettled=nil;
118
+ }
119
+ - (void)cancelAnimation { [self.clock invalidate];self.clock=nil;self.closing=NO; }
120
+ - (void)springX:(double)x y:(double)y scale:(double)scale vx:(double)vx vy:(double)vy animated:(BOOL)animated close:(BOOL)close {
121
+ [self cancelAnimation];
122
+ if(!animated || self.reduced){
123
+ self.x=x;self.y=y;self.scale=scale;self.dismissProgress=0;[self apply];
124
+ if(close)[self emitDismiss:0];else if(self.onSettled)self.onSettled(x,y,scale);
125
+ return;
126
+ }
127
+ self.startX=self.x;self.startY=self.y;self.startScale=self.scale;self.startProgress=self.dismissProgress;
128
+ self.targetX=x;self.targetY=y;self.targetScale=scale;self.vx=vx;self.vy=vy;self.closing=close;
129
+ self.startTime=CACurrentMediaTime();self.clock=[CADisplayLink displayLinkWithTarget:self selector:@selector(tick:)];
130
+ self.clock.preferredFramesPerSecond=MIN(120,self.viewport.window.screen.maximumFramesPerSecond);
131
+ [self.clock addToRunLoop:NSRunLoop.mainRunLoop forMode:NSRunLoopCommonModes];
132
+ }
133
+ - (void)tick:(CADisplayLink *)clock {
134
+ double t=MIN(.55,MAX(0,CACurrentMediaTime()-self.startTime)),decay=exp(-16*t);
135
+ self.x=self.targetX+((self.startX-self.targetX)+(self.vx+16*(self.startX-self.targetX))*t)*decay;
136
+ self.y=self.targetY+((self.startY-self.targetY)+(self.vy+16*(self.startY-self.targetY))*t)*decay;
137
+ self.scale=self.targetScale+(self.startScale-self.targetScale)*(1+16*t)*decay;
138
+ self.dismissProgress=self.startProgress*(1+16*t)*decay;
139
+ if(t>=.55){self.x=self.targetX;self.y=self.targetY;self.scale=self.targetScale;self.dismissProgress=0;}
140
+ [self apply];
141
+ if(t>=.55){BOOL close=self.closing;[self cancelAnimation];if(close)[self emitDismiss:0];else if(self.onSettled)self.onSettled(self.x,self.y,self.scale);}
142
+ }
143
+ - (void)settle:(BOOL)animated {
144
+ if(self.disposed)return;
145
+ double s=JoltClamp(self.scale,self.minimum,self.maximum);CGRect b=[self boundsFor:s];
146
+ double x=JoltClamp(self.x+self.vx*.12,CGRectGetMinX(b),CGRectGetMaxX(b));
147
+ double y=JoltClamp(self.y+self.vy*.12,CGRectGetMinY(b),CGRectGetMaxY(b));
148
+ if(self.mode==2){s=self.minimum;b=[self boundsFor:s];x=JoltClamp(0,CGRectGetMinX(b),CGRectGetMaxX(b));y=JoltClamp(0,CGRectGetMinY(b),CGRectGetMaxY(b));}
149
+ [self springX:x y:y scale:s vx:self.vx vy:self.vy animated:animated close:NO];
150
+ }
151
+ - (void)emitDismiss:(double)velocity {
152
+ if(self.disposed || self.dismissPending)return;
153
+ self.dismissPending=YES;
154
+ if(self.onDismiss)self.onDismiss(self.x,self.y,self.scale,velocity,self.dismissProgress);
155
+ }
156
+ - (void)pinched:(UIPinchGestureRecognizer *)gesture {
157
+ if(self.disposed || self.dismissPending)return;
158
+ CGPoint focus=[gesture locationInView:self.viewport];
159
+ if(gesture.state==UIGestureRecognizerStateBegan){
160
+ [self cancelAnimation];self.mode=1;self.pinchFloor=MIN(self.minimum,self.scale);self.lastFocus=focus;gesture.scale=1;
161
+ } else if(gesture.state==UIGestureRecognizerStateChanged){
162
+ double next=JoltClamp(self.scale*gesture.scale,self.pinchFloor,self.maximum);
163
+ self.x=focus.x-self.anchor.x-(self.lastFocus.x-self.anchor.x-self.x)*next/self.scale;
164
+ self.y=focus.y-self.anchor.y-(self.lastFocus.y-self.anchor.y-self.y)*next/self.scale;
165
+ self.scale=next;self.lastFocus=focus;gesture.scale=1;
166
+ self.dismissProgress=JoltClamp((self.minimum-self.scale)/(self.minimum*.15),0,1);
167
+ [self rebasePan];[self apply];
168
+ } else if(gesture.state==UIGestureRecognizerStateEnded || gesture.state==UIGestureRecognizerStateCancelled){
169
+ self.lastPan=[self.pan translationInView:self.viewport];self.touches=self.pan.numberOfTouches;
170
+ self.vx=0;self.vy=0;
171
+ if(self.pan.state!=UIGestureRecognizerStateChanged && self.pan.state!=UIGestureRecognizerStateBegan)[self settle:YES];
172
+ }
173
+ }
174
+ - (void)panned:(UIPanGestureRecognizer *)gesture {
175
+ if(self.disposed || self.dismissPending)return;
176
+ CGPoint translation=[gesture translationInView:self.viewport];
177
+ if(gesture.state==UIGestureRecognizerStateBegan){
178
+ [self cancelAnimation];self.vx=0;self.vy=0;self.lastPan=CGPointZero;self.touches=gesture.numberOfTouches;
179
+ [self rebasePan];
180
+ self.mode=self.scale<=self.minimum+.001 && self.pinch.state!=UIGestureRecognizerStateBegan && self.pinch.state!=UIGestureRecognizerStateChanged?2:1;
181
+ }
182
+ if(gesture.state==UIGestureRecognizerStateBegan || gesture.state==UIGestureRecognizerStateChanged){
183
+ if(self.pinch.state==UIGestureRecognizerStateBegan || self.pinch.state==UIGestureRecognizerStateChanged || self.touches!=gesture.numberOfTouches){
184
+ self.lastPan=translation;self.touches=gesture.numberOfTouches;[self rebasePan];return;
185
+ }
186
+ double dx=translation.x-self.lastPan.x,dy=translation.y-self.lastPan.y;
187
+ self.lastPan=translation;
188
+ if(self.mode==2){
189
+ self.x+=dx*.4;self.y+=dy;self.dismissProgress=JoltClamp(fabs(self.y)/MAX(self.viewportSize.height*.5,1),0,1);
190
+ self.scale=self.minimum*(1-self.dismissProgress*.15);
191
+ }else{
192
+ CGRect b=[self boundsFor:self.scale];
193
+ self.rawPan=CGPointMake(self.rawPan.x+dx,self.rawPan.y+dy);
194
+ self.x=JoltRubber(self.rawPan.x,CGRectGetMinX(b),CGRectGetMaxX(b),self.viewportSize.width);
195
+ self.y=JoltRubber(self.rawPan.y,CGRectGetMinY(b),CGRectGetMaxY(b),self.viewportSize.height);
196
+ }
197
+ [self apply];
198
+ }else if(gesture.state==UIGestureRecognizerStateEnded || gesture.state==UIGestureRecognizerStateCancelled || gesture.state==UIGestureRecognizerStateFailed){
199
+ CGPoint velocity=[gesture velocityInView:self.viewport];self.vx=velocity.x;self.vy=velocity.y;
200
+ BOOL commit=gesture.state==UIGestureRecognizerStateEnded && self.mode==2 &&
201
+ (self.dismissProgress>.35 || (fabs(velocity.y)>1000 && velocity.y*self.y>0));
202
+ if(commit)[self emitDismiss:velocity.y];
203
+ else if(self.pinch.state!=UIGestureRecognizerStateChanged && self.pinch.state!=UIGestureRecognizerStateBegan)[self settle:YES];
204
+ }
205
+ }
206
+ - (void)tapped:(UITapGestureRecognizer *)gesture {
207
+ if(gesture.state!=UIGestureRecognizerStateEnded || self.dismissPending)return;
208
+ CGPoint point=[gesture locationInView:self.viewport];
209
+ [self zoom:self.scale>self.minimum+.01?self.minimum:MIN(self.maximum,self.doubleTap) focusX:point.x focusY:point.y animated:YES];
210
+ }
211
+ - (void)rebasePan {
212
+ CGRect b=[self boundsFor:self.scale];
213
+ double ex=JoltClamp(self.x,CGRectGetMinX(b),CGRectGetMaxX(b)),ey=JoltClamp(self.y,CGRectGetMinY(b),CGRectGetMaxY(b));
214
+ double dx=self.x-ex,dy=self.y-ey;
215
+ self.rawPan=CGPointMake(ex+dx/(.55*MAX(.001,1-fabs(dx)/MAX(1,self.viewportSize.width))),
216
+ ey+dy/(.55*MAX(.001,1-fabs(dy)/MAX(1,self.viewportSize.height))));
217
+ }
218
+ - (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gesture {
219
+ if(self.disposed || !self.active || self.dismissPending)return NO;
220
+ if(gesture==self.pan && self.scale<=self.minimum+.001){
221
+ CGPoint velocity=[self.pan velocityInView:self.viewport];
222
+ return self.dismiss && fabs(velocity.y)>fabs(velocity.x)*1.2;
223
+ }
224
+ return YES;
225
+ }
226
+ - (BOOL)gestureRecognizer:(UIGestureRecognizer *)one shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)other {
227
+ return (one==self.pan && other==self.pinch)||(one==self.pinch && other==self.pan);
228
+ }
229
+ @end
@@ -0,0 +1,5 @@
1
+ module JoltMotion {
2
+ header "JoltImageEffect.h"
3
+ header "JoltPhotoZoom.h"
4
+ export *
5
+ }
@@ -0,0 +1,142 @@
1
+ declare namespace org.rafaelaugusto.jolt {
2
+ class PhotoZoom {
3
+ constructor(
4
+ image: android.widget.ImageView,
5
+ viewport: android.view.View,
6
+ bitmapWidth: number,
7
+ bitmapHeight: number,
8
+ minimum: number,
9
+ maximum: number,
10
+ doubleTap: number,
11
+ dismiss: boolean,
12
+ reduced: boolean,
13
+ backdrop: android.view.View | null,
14
+ chrome: android.view.View | null,
15
+ previousTouch: android.view.View.OnTouchListener | null,
16
+ previousImageTouch: android.view.View.OnTouchListener | null,
17
+ listener: PhotoZoom.Listener,
18
+ )
19
+ getX(): number
20
+ getY(): number
21
+ getScale(): number
22
+ getDismissProgress(): number
23
+ isDismissPending(): boolean
24
+ zoom(scale: number, x: number, y: number, animated: boolean): void
25
+ reset(animated: boolean): void
26
+ requestDismiss(): void
27
+ setReduced(value: boolean): void
28
+ setActive(value: boolean): void
29
+ stop(): void
30
+ }
31
+ namespace PhotoZoom {
32
+ class Listener {
33
+ constructor(implementation: {
34
+ onDismiss(
35
+ x: number,
36
+ y: number,
37
+ scale: number,
38
+ velocity: number,
39
+ progress: number,
40
+ ): void
41
+ onSettled(x: number, y: number, scale: number): void
42
+ })
43
+ }
44
+ }
45
+ class ImageEffect {
46
+ constructor(
47
+ image: android.widget.ImageView,
48
+ source: android.graphics.Bitmap | null,
49
+ destination: android.graphics.Bitmap | null,
50
+ geometry: androidNative.Array<number>,
51
+ mode: number,
52
+ x: number,
53
+ y: number,
54
+ amplitude: number,
55
+ radius: number,
56
+ duration: number,
57
+ seed: number,
58
+ previousTouch: android.view.View.OnTouchListener | null,
59
+ listener: ImageEffect.Listener,
60
+ )
61
+ isSupported(): boolean
62
+ getFailureReason(): string
63
+ getProgress(): number
64
+ getFrames(): number
65
+ getTextureBytes(): number
66
+ getLongestFrameNanos(): number
67
+ play(endpoint: number): void
68
+ pause(): void
69
+ resume(): void
70
+ seek(progress: number): void
71
+ finish(endpoint: number): void
72
+ stop(): void
73
+ stopPreservingImage(): void
74
+ }
75
+ namespace ImageEffect {
76
+ class Listener {
77
+ constructor(implementation: { onEnd(completed: boolean, progress: number): void })
78
+ }
79
+ }
80
+ class KeyboardFollower {
81
+ constructor(
82
+ view: android.view.View,
83
+ offset: number,
84
+ listener: KeyboardFollower.Listener,
85
+ )
86
+ attach(): void
87
+ detach(): void
88
+ }
89
+ namespace KeyboardFollower {
90
+ class Listener {
91
+ constructor(implementation: {
92
+ onStart(): void
93
+ onSettled(translation: number): void
94
+ })
95
+ }
96
+ }
97
+ class TextReveal {
98
+ constructor(
99
+ view: android.widget.TextView,
100
+ byWord: boolean,
101
+ duration: number,
102
+ stagger: number,
103
+ delay: number,
104
+ offset: number,
105
+ callback: TextReveal.Callback,
106
+ )
107
+ isSupported(): boolean
108
+ start(): void
109
+ cancel(): void
110
+ }
111
+ namespace TextReveal {
112
+ class Callback {
113
+ constructor(implementation: { onEnd(finished: boolean): void })
114
+ }
115
+ }
116
+ class NumberRoll {
117
+ constructor(
118
+ view: android.widget.TextView,
119
+ starts: androidNative.Array<number>,
120
+ ends: androidNative.Array<number>,
121
+ previous: androidNative.Array<string>,
122
+ direction: number,
123
+ times: androidNative.Array<number>,
124
+ values: androidNative.Array<number>,
125
+ duration: number,
126
+ delay: number,
127
+ stagger: number,
128
+ shift: number,
129
+ fade: number,
130
+ tabular: boolean,
131
+ callback: NumberRoll.Callback,
132
+ )
133
+ isSupported(): boolean
134
+ start(): void
135
+ cancel(): void
136
+ }
137
+ namespace NumberRoll {
138
+ class Callback {
139
+ constructor(implementation: { onEnd(finished: boolean): void })
140
+ }
141
+ }
142
+ }
package/src/animate.ts CHANGED
@@ -5,6 +5,7 @@ import {
5
5
  } from './decoration.js'
6
6
  import { playbackGroup } from './playback-group.js'
7
7
  import type { PlaybackAnimation } from './playback.js'
8
+ import { preparePath, type PreparedPath } from './path.js'
8
9
  import { View } from '@nativescript/core'
9
10
  import {
10
11
  isColorProperty,
@@ -38,13 +39,14 @@ import type {
38
39
  DecayVelocity,
39
40
  Keyframes,
40
41
  KeyframeValue,
42
+ KeyframeProperty,
41
43
  Stagger,
42
44
  Target,
43
45
  } from './types.js'
44
46
 
45
47
  interface Options {
46
48
  base: Transition
47
- overrides: Partial<Record<keyof Keyframes, Transition>>
49
+ overrides: Partial<Record<KeyframeProperty | 'path', Transition>>
48
50
  stagger: number | Stagger | undefined
49
51
  relative: boolean | undefined
50
52
  haptic: Haptic | undefined
@@ -65,7 +67,7 @@ export function animate(
65
67
  animation: Keyframes | PresetName,
66
68
  options?: AnimateOptions | SpringName,
67
69
  ): PlaybackAnimation {
68
- const { keyframes, base, overrides, stagger, relative, haptic } = prepare(
70
+ const { keyframes, base, overrides, stagger, relative, haptic, path } = prepare(
69
71
  animation,
70
72
  options,
71
73
  )
@@ -78,7 +80,7 @@ export function animate(
78
80
  else if (isDecorationProperty(property)) validateDecorationTarget(view, property)
79
81
  return new ViewAnimation(
80
82
  view,
81
- plan(keyframes, base, overrides, 0, relative),
83
+ plan(keyframes, base, overrides, 0, relative, path),
82
84
  feedback,
83
85
  )
84
86
  }
@@ -88,23 +90,19 @@ export function animate(
88
90
  for (const property of Object.keys(keyframes))
89
91
  if (isColorProperty(property)) validateColorTarget(view, property)
90
92
  else if (isDecorationProperty(property)) validateDecorationTarget(view, property)
91
- return playbackGroup(
92
- views.map(
93
- (view, index) =>
94
- new ViewAnimation(
95
- view,
96
- plan(
97
- keyframes,
98
- base,
99
- overrides,
100
- staggerDelay(stagger, index, views.length),
101
- relative,
102
- ),
103
- feedback,
104
- true,
105
- ),
93
+ const plans = views.map((_, index) =>
94
+ plan(
95
+ keyframes,
96
+ base,
97
+ overrides,
98
+ staggerDelay(stagger, index, views.length),
99
+ relative,
100
+ path,
106
101
  ),
107
102
  )
103
+ return playbackGroup(
104
+ views.map((view, index) => new ViewAnimation(view, plans[index]!, feedback, true)),
105
+ )
108
106
  }
109
107
 
110
108
  /** Starts at the current translation and slows down from a velocity in DIPs per second. */
@@ -183,7 +181,7 @@ export function decay(
183
181
  /** Stops all or selected properties at their current values. `scale` stops both axes. */
184
182
  export function stop(
185
183
  target: Target | readonly Target[],
186
- properties?: readonly (keyof Keyframes)[],
184
+ properties?: readonly KeyframeProperty[],
187
185
  ) {
188
186
  const expanded = properties?.flatMap<Property>((property) =>
189
187
  property === 'scale' ? ['scaleX', 'scaleY'] : [property],
@@ -225,6 +223,17 @@ function isList<T>(target: T | readonly T[]): target is readonly T[] {
225
223
  function prepare(
226
224
  animation: Keyframes | PresetName,
227
225
  options?: AnimateOptions | SpringName,
226
+ ) {
227
+ const prepared = resolveAnimation(animation, options)
228
+ return {
229
+ ...prepared,
230
+ path: preparePath(prepared.keyframes, prepared.base, prepared.overrides),
231
+ }
232
+ }
233
+
234
+ function resolveAnimation(
235
+ animation: Keyframes | PresetName,
236
+ options?: AnimateOptions | SpringName,
228
237
  ) {
229
238
  const user = normalize(options)
230
239
  if (typeof animation !== 'string') return { keyframes: animation, ...user }
@@ -271,7 +280,7 @@ function normalize(options: AnimateOptions | SpringName | undefined): Options {
271
280
  if (key === 'stagger') stagger = value as Options['stagger']
272
281
  else if (key === 'relative') relative = value as Options['relative']
273
282
  else if (key === 'haptic') haptic = value as Options['haptic']
274
- else if (key === 'scale' || isProperty(key)) {
283
+ else if (key === 'path' || key === 'scale' || isProperty(key)) {
275
284
  overrides[key] = typeof value === 'string' ? spring(value as SpringName) : value
276
285
  } else base[key] = value
277
286
  }
@@ -284,8 +293,25 @@ function plan(
284
293
  overrides: Options['overrides'],
285
294
  offset: number,
286
295
  relative = false,
296
+ path?: PreparedPath,
287
297
  ): Plan[] {
288
- return expand(keyframes).map(([property, frames]) => {
298
+ return expand(keyframes, path).map(([property, frames]) => {
299
+ if (path?.properties.includes(property as 'x' | 'y' | 'rotate')) {
300
+ if (
301
+ !Number.isFinite(path.transition.delay + offset) ||
302
+ path.transition.delay + offset < 0
303
+ )
304
+ throw new RangeError(
305
+ 'Path delays, including stagger, must be finite and nonnegative.',
306
+ )
307
+ return {
308
+ property,
309
+ keyframes: frames,
310
+ transition: { ...path.transition, delay: path.transition.delay + offset },
311
+ relative,
312
+ path,
313
+ }
314
+ }
289
315
  const override =
290
316
  overrides[property] ?? (property.startsWith('scale') ? overrides.scale : undefined)
291
317
  const transition = resolveTransition(
@@ -303,31 +329,55 @@ export function startingValues(
303
329
  animation: Keyframes | PresetName,
304
330
  options?: AnimateOptions | SpringName,
305
331
  ) {
306
- const { keyframes, relative } = prepare(animation, options)
332
+ const { keyframes, relative, path } = prepare(animation, options)
307
333
  if (!keyframes) throw new Error(`Unknown preset "${animation}".`)
308
334
  return new Map(
309
- expand(keyframes).map(([property, [first]]) => [
335
+ expand(keyframes, path).map(([property, [first]]) => [
310
336
  property,
311
337
  relative && !isColorProperty(property) ? null : first!,
312
338
  ]),
313
339
  )
314
340
  }
315
341
 
316
- function expand(keyframes: Keyframes): [Property, (number | null)[]][] {
342
+ /** Each property the animation touches, with the value it leaves behind when that is known up front. */
343
+ export function endingValues(
344
+ animation: Keyframes | PresetName,
345
+ options?: AnimateOptions | SpringName,
346
+ ) {
347
+ const { keyframes, base, overrides, relative, path } = prepare(animation, options)
348
+ return new Map(
349
+ plan(keyframes, base, overrides, 0, relative, path).map(
350
+ ({ property, keyframes: frames, transition, path: owned }) => {
351
+ const finite = Number.isFinite(transition.repeat)
352
+ const backwards = transition.reverse && finite && transition.repeat % 2 === 1
353
+ const unknown = owned !== undefined || (relative && !isColorProperty(property))
354
+ const frame = !finite || backwards ? frames[0] : frames.at(-1)
355
+ return [property, unknown ? null : frame!]
356
+ },
357
+ ),
358
+ )
359
+ }
360
+
361
+ function expand(
362
+ keyframes: Keyframes,
363
+ path?: PreparedPath,
364
+ ): [Property, (number | null)[]][] {
317
365
  const expanded: [Property, (number | null)[]][] = []
318
- for (const [key, value] of Object.entries(keyframes) as [
319
- keyof Keyframes,
320
- KeyframeValue | ColorKeyframes | undefined,
321
- ][]) {
322
- if (value === undefined) continue
366
+ for (const [key, value] of Object.entries(keyframes)) {
367
+ if (value === undefined || key === 'path' || key === 'orient') continue
323
368
  if (key !== 'scale' && !isProperty(key)) throw new Error(`Can't animate "${key}".`)
324
369
 
325
370
  const properties: Property[] = key === 'scale' ? ['scaleX', 'scaleY'] : [key]
326
371
  for (const property of properties) {
327
372
  if (key === 'scale' && keyframes[property] !== undefined) continue
328
- expanded.push([property, toKeyframes(property, value)])
373
+ expanded.push([
374
+ property,
375
+ toKeyframes(property, value as KeyframeValue | ColorKeyframes),
376
+ ])
329
377
  }
330
378
  }
379
+ for (const [property, value] of path?.starts ?? [])
380
+ expanded.push([property, [value, value]])
331
381
  return expanded
332
382
  }
333
383
 
package/src/config.ts ADDED
@@ -0,0 +1,46 @@
1
+ import { getDriver } from './driver/index.js'
2
+
3
+ export interface JoltConfig {
4
+ /** Playback speed for every engine animation. `0.2` plays everything five times slower. */
5
+ timeScale?: number
6
+ /**
7
+ * When transforms, paths, decorations and decay jump to their end instead of animating.
8
+ * `user` follows the platform setting. Opacity and colors still animate.
9
+ */
10
+ reducedMotion?: 'user' | 'always' | 'never'
11
+ }
12
+
13
+ const settings: Required<JoltConfig> = { timeScale: 1, reducedMotion: 'user' }
14
+ const preferences: readonly JoltConfig['reducedMotion'][] = ['user', 'always', 'never']
15
+
16
+ /**
17
+ * Global settings, applied to animations that start after the call.
18
+ *
19
+ * ```ts
20
+ * configure({ timeScale: 0.2 }) // tune a spring in slow motion
21
+ * configure({ reducedMotion: 'always' }) // check the reduced layout
22
+ * ```
23
+ */
24
+ export function configure(options: JoltConfig): void {
25
+ if (options === null || typeof options !== 'object')
26
+ throw new TypeError('configure() takes an options object.')
27
+ const { timeScale, reducedMotion } = options
28
+ if (timeScale !== undefined && (!Number.isFinite(timeScale) || timeScale <= 0))
29
+ throw new RangeError(`Time scale must be a positive finite number, got ${timeScale}.`)
30
+ if (reducedMotion !== undefined && !preferences.includes(reducedMotion))
31
+ throw new TypeError(
32
+ `Reduced motion must be 'user', 'always' or 'never', got ${String(reducedMotion)}.`,
33
+ )
34
+ if (timeScale !== undefined) settings.timeScale = timeScale
35
+ if (reducedMotion !== undefined) settings.reducedMotion = reducedMotion
36
+ }
37
+
38
+ export function timeScale(): number {
39
+ return settings.timeScale
40
+ }
41
+
42
+ /** Whether motion is reduced right now. `user` asks the platform each time. */
43
+ export function reducedMotion(): boolean {
44
+ if (settings.reducedMotion === 'user') return getDriver().reducedMotion?.() ?? false
45
+ return settings.reducedMotion === 'always'
46
+ }
@@ -207,5 +207,19 @@ export function createAndroidDriver(): Driver {
207
207
  marker.cancel()
208
208
  }
209
209
  },
210
+
211
+ reducedMotion() {
212
+ // Remove animations sets the animator duration scale to zero.
213
+ if (android.os.Build.VERSION.SDK_INT >= 26)
214
+ return !android.animation.ValueAnimator.areAnimatorsEnabled()
215
+ const { Global } = android.provider.Settings
216
+ return (
217
+ Global.getFloat(
218
+ Utils.android.getApplicationContext().getContentResolver(),
219
+ Global.ANIMATOR_DURATION_SCALE,
220
+ 1,
221
+ ) === 0
222
+ )
223
+ },
210
224
  }
211
225
  }
@@ -34,6 +34,8 @@ export interface Driver {
34
34
  presented(view: View, property: Property): number | undefined
35
35
  /** Calls back after `ms` of animation time, respecting platform speed settings. */
36
36
  after(ms: number, callback: () => void): () => void
37
+ /** The platform's accessibility setting: Reduce Motion on iOS, Remove animations on Android. */
38
+ reducedMotion?(): boolean
37
39
  }
38
40
 
39
41
  export function after(ms: number, callback: () => void) {
@@ -203,7 +203,6 @@ export function playIOSColor(
203
203
  )
204
204
  animation.keyTimes = NSArray.arrayWithArray(track.times as number[])
205
205
  animation.duration = Math.max(track.duration, 1) / 1000
206
- timing.configure(animation, layer)
207
206
  if (animation.respondsToSelector('setPreferredFrameRateRange:')) {
208
207
  const maximum = Math.min(
209
208
  120,
@@ -220,6 +219,7 @@ export function playIOSColor(
220
219
  animation.autoreverses = track.reverse
221
220
  animation.repeatCount = track.reverse ? (track.repeat + 1) / 2 : track.repeat + 1
222
221
  }
222
+ timing.configure(animation, layer)
223
223
  CATransaction.begin()
224
224
  if (!timing.paused && track.repeat !== Infinity)
225
225
  CATransaction.setCompletionBlock(() => {