@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.
- package/README.md +122 -5
- package/dist/animate.d.ts +4 -2
- package/dist/animate.d.ts.map +1 -1
- package/dist/animate.js +48 -11
- package/dist/animate.js.map +1 -1
- package/dist/config.d.ts +22 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +34 -0
- package/dist/config.js.map +1 -0
- package/dist/driver/android.d.ts.map +1 -1
- package/dist/driver/android.js +7 -0
- package/dist/driver/android.js.map +1 -1
- package/dist/driver/index.d.ts +2 -0
- package/dist/driver/index.d.ts.map +1 -1
- package/dist/driver/index.js.map +1 -1
- package/dist/driver/ios-color.js +1 -1
- package/dist/driver/ios-color.js.map +1 -1
- package/dist/driver/ios-decoration.js +1 -1
- package/dist/driver/ios-decoration.js.map +1 -1
- package/dist/driver/ios-playback.d.ts +1 -1
- package/dist/driver/ios-playback.d.ts.map +1 -1
- package/dist/driver/ios-playback.js +14 -0
- package/dist/driver/ios-playback.js.map +1 -1
- package/dist/driver/ios.d.ts.map +1 -1
- package/dist/driver/ios.js +1 -0
- package/dist/driver/ios.js.map +1 -1
- package/dist/engine.d.ts +16 -1
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +33 -1
- package/dist/engine.js.map +1 -1
- package/dist/image-effect-native.d.ts +26 -0
- package/dist/image-effect-native.d.ts.map +1 -0
- package/dist/image-effect-native.js +73 -0
- package/dist/image-effect-native.js.map +1 -0
- package/dist/image-effect.d.ts +50 -0
- package/dist/image-effect.d.ts.map +1 -0
- package/dist/image-effect.js +290 -0
- package/dist/image-effect.js.map +1 -0
- package/dist/image-owner.d.ts +3 -0
- package/dist/image-owner.d.ts.map +1 -0
- package/dist/image-owner.js +11 -0
- package/dist/image-owner.js.map +1 -0
- package/dist/index.d.ts +15 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -1
- package/dist/items.d.ts +20 -0
- package/dist/items.d.ts.map +1 -0
- package/dist/items.js +109 -0
- package/dist/items.js.map +1 -0
- package/dist/keyboard.d.ts +17 -0
- package/dist/keyboard.d.ts.map +1 -0
- package/dist/keyboard.js +116 -0
- package/dist/keyboard.js.map +1 -0
- package/dist/label.d.ts +7 -0
- package/dist/label.d.ts.map +1 -0
- package/dist/label.js +60 -0
- package/dist/label.js.map +1 -0
- package/dist/liquid.d.ts.map +1 -1
- package/dist/liquid.js +6 -1
- package/dist/liquid.js.map +1 -1
- package/dist/loader.d.ts.map +1 -1
- package/dist/loader.js +2 -1
- package/dist/loader.js.map +1 -1
- package/dist/particles.d.ts.map +1 -1
- package/dist/particles.js +4 -3
- package/dist/particles.js.map +1 -1
- package/dist/path.d.ts +12 -0
- package/dist/path.d.ts.map +1 -0
- package/dist/path.js +90 -0
- package/dist/path.js.map +1 -0
- package/dist/playback-group.d.ts.map +1 -1
- package/dist/playback-group.js +29 -1
- package/dist/playback-group.js.map +1 -1
- package/dist/press.d.ts.map +1 -1
- package/dist/press.js +3 -3
- package/dist/press.js.map +1 -1
- package/dist/reveal.d.ts.map +1 -1
- package/dist/reveal.js +3 -2
- package/dist/reveal.js.map +1 -1
- package/dist/roll-diff.d.ts +22 -0
- package/dist/roll-diff.d.ts.map +1 -0
- package/dist/roll-diff.js +36 -0
- package/dist/roll-diff.js.map +1 -0
- package/dist/roll.d.ts +29 -0
- package/dist/roll.d.ts.map +1 -0
- package/dist/roll.js +442 -0
- package/dist/roll.js.map +1 -0
- package/dist/scroll-linked.d.ts +10 -0
- package/dist/scroll-linked.d.ts.map +1 -0
- package/dist/scroll-linked.js +58 -0
- package/dist/scroll-linked.js.map +1 -0
- package/dist/sequence.d.ts +10 -5
- package/dist/sequence.d.ts.map +1 -1
- package/dist/sequence.js +202 -14
- package/dist/sequence.js.map +1 -1
- package/dist/shimmer.d.ts.map +1 -1
- package/dist/shimmer.js +2 -1
- package/dist/shimmer.js.map +1 -1
- package/dist/svg-path.d.ts +14 -0
- package/dist/svg-path.d.ts.map +1 -0
- package/dist/svg-path.js +281 -0
- package/dist/svg-path.js.map +1 -0
- package/dist/symbol-effect.d.ts.map +1 -1
- package/dist/symbol-effect.js +4 -3
- package/dist/symbol-effect.js.map +1 -1
- package/dist/text-layout.d.ts +8 -0
- package/dist/text-layout.d.ts.map +1 -1
- package/dist/text-layout.js +8 -2
- package/dist/text-layout.js.map +1 -1
- package/dist/text.d.ts +1 -1
- package/dist/text.d.ts.map +1 -1
- package/dist/text.js +60 -64
- package/dist/text.js.map +1 -1
- package/dist/track.d.ts +2 -0
- package/dist/track.d.ts.map +1 -1
- package/dist/track.js +62 -15
- package/dist/track.js.map +1 -1
- package/dist/types.d.ts +7 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/vue/index.d.ts +89 -0
- package/dist/vue/index.d.ts.map +1 -0
- package/dist/vue/index.js +134 -0
- package/dist/vue/index.js.map +1 -0
- package/dist/zoom-native.d.ts +30 -0
- package/dist/zoom-native.d.ts.map +1 -0
- package/dist/zoom-native.js +56 -0
- package/dist/zoom-native.js.map +1 -0
- package/dist/zoomable.d.ts +48 -0
- package/dist/zoomable.d.ts.map +1 -0
- package/dist/zoomable.js +249 -0
- package/dist/zoomable.js.map +1 -0
- package/package.json +16 -3
- package/platforms/android/java/org/rafaelaugusto/jolt/ImageEffect.java +288 -0
- package/platforms/android/java/org/rafaelaugusto/jolt/KeyboardFollower.java +91 -0
- package/platforms/android/java/org/rafaelaugusto/jolt/NumberRoll.java +190 -0
- package/platforms/android/java/org/rafaelaugusto/jolt/PhotoZoom.java +250 -0
- package/platforms/android/java/org/rafaelaugusto/jolt/TextReveal.java +180 -0
- package/platforms/android/proguard-rules.pro +10 -0
- package/platforms/ios/build.xcconfig +1 -0
- package/platforms/ios/src/JoltImageEffect.h +25 -0
- package/platforms/ios/src/JoltImageEffect.mm +242 -0
- package/platforms/ios/src/JoltPhotoZoom.h +23 -0
- package/platforms/ios/src/JoltPhotoZoom.m +229 -0
- package/platforms/ios/src/module.modulemap +5 -0
- package/src/android-plugin.d.ts +142 -0
- package/src/animate.ts +80 -30
- package/src/config.ts +46 -0
- package/src/driver/android.ts +14 -0
- package/src/driver/index.ts +2 -0
- package/src/driver/ios-color.ts +1 -1
- package/src/driver/ios-decoration.ts +1 -1
- package/src/driver/ios-playback.ts +15 -1
- package/src/driver/ios.ts +2 -0
- package/src/engine.ts +47 -2
- package/src/image-effect-native.ts +129 -0
- package/src/image-effect.ts +336 -0
- package/src/image-owner.ts +12 -0
- package/src/index.ts +20 -1
- package/src/ios-plugin.d.ts +59 -0
- package/src/items.ts +140 -0
- package/src/keyboard.ts +132 -0
- package/src/label.ts +76 -0
- package/src/liquid.ts +6 -1
- package/src/loader.ts +2 -1
- package/src/particles.ts +4 -4
- package/src/path.ts +124 -0
- package/src/playback-group.ts +34 -1
- package/src/press.ts +3 -5
- package/src/reveal.ts +3 -3
- package/src/roll-diff.ts +57 -0
- package/src/roll.ts +515 -0
- package/src/scroll-linked.ts +73 -0
- package/src/sequence.ts +199 -14
- package/src/shimmer.ts +2 -1
- package/src/svg-path.ts +355 -0
- package/src/symbol-effect.ts +4 -3
- package/src/text-layout.ts +14 -2
- package/src/text.ts +62 -81
- package/src/track.ts +89 -24
- package/src/types.ts +8 -1
- package/src/vue/index.ts +189 -0
- package/src/zoom-native.ts +124 -0
- 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,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<
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
|
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
|
-
|
|
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)
|
|
319
|
-
|
|
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([
|
|
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
|
+
}
|
package/src/driver/android.ts
CHANGED
|
@@ -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
|
}
|
package/src/driver/index.ts
CHANGED
|
@@ -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) {
|
package/src/driver/ios-color.ts
CHANGED
|
@@ -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(() => {
|