@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,180 @@
1
+ package org.rafaelaugusto.jolt;
2
+
3
+ import android.animation.Animator;
4
+ import android.animation.AnimatorListenerAdapter;
5
+ import android.animation.ValueAnimator;
6
+ import android.text.SpannableStringBuilder;
7
+ import android.text.Spanned;
8
+ import android.text.TextPaint;
9
+ import android.text.method.TransformationMethod;
10
+ import android.text.style.CharacterStyle;
11
+ import android.text.style.UpdateAppearance;
12
+ import android.view.animation.Interpolator;
13
+ import android.view.animation.LinearInterpolator;
14
+ import android.view.animation.PathInterpolator;
15
+ import android.widget.TextView;
16
+ import java.text.BreakIterator;
17
+ import java.util.ArrayList;
18
+ import java.util.List;
19
+
20
+ /** Fades a TextView's text in piece by piece. Layout, truncation and the real text stay as they are. */
21
+ public final class TextReveal {
22
+ public interface Callback {
23
+ void onEnd(boolean finished);
24
+ }
25
+
26
+ private static final int MAX_PIECES = 256;
27
+ private static final Interpolator EASE_OUT = new PathInterpolator(0f, 0f, 0.58f, 1f);
28
+ private final TextView view;
29
+ private final long duration;
30
+ private final long stagger;
31
+ private final long delay;
32
+ private final float offset;
33
+ private final Callback callback;
34
+ private final CharSequence original;
35
+ private final CharSequence shown;
36
+ private final TransformationMethod method;
37
+ private final Piece[] pieces;
38
+ private ValueAnimator animator;
39
+ private boolean settled;
40
+
41
+ public TextReveal(TextView view, boolean byWord, long duration, long stagger, long delay, float offset, Callback callback) {
42
+ this.view = view;
43
+ this.duration = Math.max(duration, 1);
44
+ this.stagger = stagger;
45
+ this.delay = delay;
46
+ this.offset = offset;
47
+ this.callback = callback;
48
+ CharSequence text = view.getText();
49
+ this.original = text == null ? "" : text;
50
+ // NativeScript's transformation method rebuilds the displayed text from its own model,
51
+ // so pieces are measured on what is drawn and the spans go on that text.
52
+ this.method = view.getTransformationMethod();
53
+ CharSequence displayed = method != null ? method.getTransformation(original, view) : original;
54
+ this.shown = displayed == null ? "" : displayed;
55
+ this.pieces = split(shown.toString(), byWord);
56
+ }
57
+
58
+ public boolean isSupported() {
59
+ return pieces.length <= MAX_PIECES;
60
+ }
61
+
62
+ public void start() {
63
+ if (settled || animator != null) return;
64
+ if (pieces.length == 0) {
65
+ settle(true);
66
+ return;
67
+ }
68
+ SpannableStringBuilder spannable = new SpannableStringBuilder(shown);
69
+ for (Piece piece : pieces) {
70
+ piece.alpha = 0f;
71
+ piece.shift = offset;
72
+ spannable.setSpan(piece, piece.start, piece.end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
73
+ }
74
+ if (method != null) view.setTransformationMethod(null);
75
+ view.setText(spannable);
76
+ final long total = delay + duration + (pieces.length - 1) * stagger;
77
+ animator = ValueAnimator.ofFloat(0f, 1f);
78
+ animator.setDuration(total);
79
+ animator.setInterpolator(new LinearInterpolator());
80
+ animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
81
+ @Override
82
+ public void onAnimationUpdate(ValueAnimator animation) {
83
+ update(animation.getAnimatedFraction() * total);
84
+ view.invalidate();
85
+ }
86
+ });
87
+ animator.addListener(new AnimatorListenerAdapter() {
88
+ @Override
89
+ public void onAnimationEnd(Animator animation) {
90
+ settle(true);
91
+ }
92
+ });
93
+ animator.start();
94
+ }
95
+
96
+ public void cancel() {
97
+ settle(false);
98
+ }
99
+
100
+ private void update(float time) {
101
+ for (int i = 0; i < pieces.length; i++) {
102
+ float local = (time - delay - i * stagger) / duration;
103
+ float eased = local <= 0f ? 0f : local >= 1f ? 1f : EASE_OUT.getInterpolation(local);
104
+ pieces[i].alpha = eased;
105
+ pieces[i].shift = offset * (1f - eased);
106
+ }
107
+ }
108
+
109
+ private void settle(boolean finished) {
110
+ if (settled) return;
111
+ settled = true;
112
+ ValueAnimator running = animator;
113
+ animator = null;
114
+ if (running != null) running.cancel();
115
+ CharSequence current = view.getText();
116
+ // An app change already replaced the spans; its text wins.
117
+ if (current instanceof Spanned && ((Spanned) current).getSpans(0, current.length(), Piece.class).length > 0) {
118
+ view.setText(original);
119
+ }
120
+ if (method != null && view.getTransformationMethod() == null) view.setTransformationMethod(method);
121
+ callback.onEnd(finished);
122
+ }
123
+
124
+ private static Piece[] split(String text, boolean byWord) {
125
+ List<Piece> pieces = new ArrayList<>();
126
+ BreakIterator iterator = byWord ? BreakIterator.getWordInstance() : BreakIterator.getCharacterInstance();
127
+ iterator.setText(text);
128
+ int start = iterator.first();
129
+ for (int end = iterator.next(); end != BreakIterator.DONE; start = end, end = iterator.next()) {
130
+ if (byWord ? isWord(text, start, end) : !isBlank(text, start, end)) pieces.add(new Piece(start, end));
131
+ }
132
+ if (byWord) {
133
+ // Spaces and punctuation stay with the word before them, so every visible mark is revealed.
134
+ for (int i = 0; i < pieces.size(); i++) {
135
+ Piece piece = pieces.get(i);
136
+ if (i == 0) piece.start = 0;
137
+ piece.end = i + 1 < pieces.size() ? pieces.get(i + 1).start : text.length();
138
+ }
139
+ if (pieces.isEmpty() && !isBlank(text, 0, text.length())) pieces.add(new Piece(0, text.length()));
140
+ }
141
+ return pieces.toArray(new Piece[0]);
142
+ }
143
+
144
+ private static boolean isWord(String text, int start, int end) {
145
+ for (int i = start; i < end; ) {
146
+ int point = text.codePointAt(i);
147
+ if (Character.isLetterOrDigit(point)) return true;
148
+ i += Character.charCount(point);
149
+ }
150
+ return false;
151
+ }
152
+
153
+ private static boolean isBlank(String text, int start, int end) {
154
+ for (int i = start; i < end; ) {
155
+ int point = text.codePointAt(i);
156
+ if (!Character.isWhitespace(point) && !Character.isSpaceChar(point)) return false;
157
+ i += Character.charCount(point);
158
+ }
159
+ return true;
160
+ }
161
+
162
+ /** Only changes how its range is drawn, so the TextView's layout never sees it. */
163
+ private static final class Piece extends CharacterStyle implements UpdateAppearance {
164
+ int start;
165
+ int end;
166
+ float alpha;
167
+ float shift;
168
+
169
+ Piece(int start, int end) {
170
+ this.start = start;
171
+ this.end = end;
172
+ }
173
+
174
+ @Override
175
+ public void updateDrawState(TextPaint paint) {
176
+ paint.setAlpha(Math.round(paint.getAlpha() * alpha));
177
+ paint.baselineShift += Math.round(shift);
178
+ }
179
+ }
180
+ }
@@ -5,3 +5,13 @@
5
5
  public android.graphics.drawable.Drawable getWrappedDrawable();
6
6
  }
7
7
  -keep class org.rafaelaugusto.jolt.DecorationTarget { *; }
8
+ -keep class org.rafaelaugusto.jolt.KeyboardFollower { *; }
9
+ -keep class org.rafaelaugusto.jolt.KeyboardFollower$* { *; }
10
+ -keep class org.rafaelaugusto.jolt.TextReveal { *; }
11
+ -keep class org.rafaelaugusto.jolt.TextReveal$* { *; }
12
+ -keep class org.rafaelaugusto.jolt.NumberRoll { *; }
13
+ -keep class org.rafaelaugusto.jolt.NumberRoll$* { *; }
14
+ -keep class org.rafaelaugusto.jolt.ImageEffect { *; }
15
+ -keep class org.rafaelaugusto.jolt.ImageEffect$* { *; }
16
+ -keep class org.rafaelaugusto.jolt.PhotoZoom { *; }
17
+ -keep class org.rafaelaugusto.jolt.PhotoZoom$* { *; }
@@ -0,0 +1 @@
1
+ OTHER_LDFLAGS = $(inherited) -framework Metal -framework MetalKit
@@ -0,0 +1,25 @@
1
+ #import <UIKit/UIKit.h>
2
+
3
+ NS_ASSUME_NONNULL_BEGIN
4
+ @interface JoltImageEffect : NSObject
5
+ + (nullable instancetype)createWithView:(UIImageView *)view
6
+ destination:(nullable UIImage *)destination
7
+ mode:(NSInteger)mode
8
+ originX:(double)x originY:(double)y
9
+ amplitude:(double)amplitude radius:(double)radius
10
+ duration:(double)duration seed:(double)seed
11
+ listener:(void (^)(BOOL, double))listener;
12
+ @property(nonatomic, readonly) double progress;
13
+ @property(nonatomic, readonly) BOOL paused;
14
+ @property(nonatomic, readonly) NSUInteger frames;
15
+ @property(nonatomic, readonly) double longestFrame;
16
+ @property(nonatomic, readonly) NSUInteger textureBytes;
17
+ - (void)play:(double)endpoint;
18
+ - (void)pause;
19
+ - (void)resume;
20
+ - (void)seek:(double)progress;
21
+ - (void)finish:(double)endpoint;
22
+ - (void)stop;
23
+ - (void)stopPreservingImage;
24
+ @end
25
+ NS_ASSUME_NONNULL_END
@@ -0,0 +1,242 @@
1
+ #import "JoltImageEffect.h"
2
+ #import <MetalKit/MetalKit.h>
3
+ #import <simd/simd.h>
4
+
5
+ static const char *JoltImageShader = R"METAL(
6
+ #include <metal_stdlib>
7
+ using namespace metal;
8
+ struct Vertex {float4 position [[position]]; float2 uv;};
9
+ struct Params {float2 size; float2 origin; float2 displacement; float progress; float amplitude; float radius; float seed; float mode;};
10
+ vertex Vertex joltVertex(uint id [[vertex_id]]) {
11
+ float2 p[4]={float2(-1,-1),float2(1,-1),float2(-1,1),float2(1,1)};
12
+ float2 uv[4]={float2(0,1),float2(1,1),float2(0,0),float2(1,0)};
13
+ return {float4(p[id],0,1),uv[id]};
14
+ }
15
+ fragment float4 joltImage(Vertex in [[stage_in]], constant Params &u [[buffer(0)]],
16
+ texture2d<float> source [[texture(0)]], texture2d<float> destination [[texture(1)]]) {
17
+ constexpr sampler s(coord::normalized,address::clamp_to_edge,filter::linear);
18
+ float2 p=in.uv*u.size,q=p;
19
+ if(u.mode<1.5) {
20
+ float2 v=p-u.origin; float d=length(v);
21
+ float edge=clamp(min(min(p.x,u.size.x-p.x),min(p.y,u.size.y-p.y))/max(u.amplitude*2.,1.),0.,1.);
22
+ if(u.mode<.5) {
23
+ float wave=d-u.progress*length(u.size);
24
+ float a=sin(wave/max(u.radius*.12,1.))*exp(-pow(wave/max(u.radius,1.),2.))*u.amplitude*sin(u.progress*3.14159265);
25
+ q-=v/max(d,1.)*a*edge;
26
+ } else q-=u.displacement*exp(-d*d/max(u.radius*u.radius,1.))*edge;
27
+ return source.sample(s,q/u.size);
28
+ }
29
+ if(u.progress<=0.) return source.sample(s,in.uv);
30
+ if(u.progress>=1.) return destination.sample(s,in.uv);
31
+ float n=fract(sin(dot(floor(p/3.),float2(12.9898,78.233))+u.seed)*43758.5453);
32
+ float a=smoothstep(n-.08,n+.08,u.progress*1.16-.08);
33
+ return mix(source.sample(s,in.uv),destination.sample(s,in.uv),a);
34
+ }
35
+ )METAL";
36
+
37
+ typedef struct {
38
+ vector_float2 size, origin, displacement;
39
+ float progress, amplitude, radius, seed, mode;
40
+ } JoltImageParams;
41
+
42
+ @interface JoltImageEffect () <MTKViewDelegate, UIGestureRecognizerDelegate>
43
+ @property(nonatomic, weak) UIImageView *image;
44
+ @property(nonatomic, strong) UIImage *original;
45
+ @property(nonatomic, strong) MTKView *surface;
46
+ @property(nonatomic, strong) id<MTLCommandQueue> queue;
47
+ @property(nonatomic, strong) id<MTLRenderPipelineState> pipeline;
48
+ @property(nonatomic, strong) id<MTLTexture> sourceTexture, destinationTexture;
49
+ @property(nonatomic, strong) UIPanGestureRecognizer *pan;
50
+ @property(nonatomic, copy) void (^listener)(BOOL,double);
51
+ @property(nonatomic) BOOL originalInteraction, disposed, playing, settling;
52
+ @property(nonatomic, readwrite) BOOL paused;
53
+ @property(nonatomic, readwrite) double progress;
54
+ @property(nonatomic, readwrite) NSUInteger frames;
55
+ @property(nonatomic, readwrite) double longestFrame;
56
+ @property(nonatomic, readwrite) NSUInteger textureBytes;
57
+ @property(nonatomic) double duration, goal, anchor, startProgress, remaining, previousFrame;
58
+ @property(nonatomic) vector_float2 settleStart;
59
+ @property(nonatomic) JoltImageParams params;
60
+ @end
61
+
62
+ @implementation JoltImageEffect
63
+ + (instancetype)createWithView:(UIImageView *)view destination:(UIImage *)destination mode:(NSInteger)mode
64
+ originX:(double)x originY:(double)y amplitude:(double)amplitude radius:(double)radius
65
+ duration:(double)duration seed:(double)seed listener:(void (^)(BOOL,double))listener {
66
+ if(!view.image.CGImage || view.image.images.count || view.image.renderingMode==UIImageRenderingModeAlwaysTemplate || !view.window) return nil;
67
+ CGFloat scale=view.window.screen.scale;
68
+ NSUInteger w=(NSUInteger)ceil(view.bounds.size.width*scale),h=(NSUInteger)ceil(view.bounds.size.height*scale);
69
+ if(!w || !h || w*h>4194304 || (mode==2 && !destination.CGImage)) return nil;
70
+ id<MTLDevice> device=MTLCreateSystemDefaultDevice();
71
+ if(!device) return nil;
72
+ static id<MTLRenderPipelineState> cached;
73
+ static dispatch_once_t once;
74
+ dispatch_once(&once, ^{
75
+ NSError *error=nil;
76
+ id<MTLLibrary> library=[device newLibraryWithSource:[NSString stringWithUTF8String:JoltImageShader] options:nil error:&error];
77
+ MTLRenderPipelineDescriptor *descriptor=[MTLRenderPipelineDescriptor new];
78
+ descriptor.vertexFunction=[library newFunctionWithName:@"joltVertex"];
79
+ descriptor.fragmentFunction=[library newFunctionWithName:@"joltImage"];
80
+ descriptor.colorAttachments[0].pixelFormat=MTLPixelFormatBGRA8Unorm;
81
+ if(library) cached=[device newRenderPipelineStateWithDescriptor:descriptor error:&error];
82
+ if(error) NSLog(@"Jolt image shader unavailable: %@",error.localizedDescription);
83
+ });
84
+ if(!cached) return nil;
85
+ JoltImageEffect *effect=[JoltImageEffect new];
86
+ effect.image=view; effect.original=view.image; effect.originalInteraction=view.userInteractionEnabled;
87
+ effect.listener=listener; effect.pipeline=cached; effect.queue=[device newCommandQueue];
88
+ effect.duration=duration/1000.; effect.goal=1; effect.paused=YES;
89
+ effect.params=(JoltImageParams){ {(float)w,(float)h}, {(float)(x*scale),(float)(y*scale)}, {0,0}, 0,
90
+ (float)(amplitude*scale),(float)(radius*scale),(float)seed,(float)mode };
91
+ effect.sourceTexture=[effect texture:view.image device:device width:w height:h];
92
+ effect.destinationTexture=mode==2?[effect texture:destination device:device width:w height:h]:effect.sourceTexture;
93
+ if(!effect.sourceTexture || !effect.destinationTexture) return nil;
94
+ effect.textureBytes=w*h*4*(mode==2?2:1);
95
+ MTKView *surface=[[MTKView alloc] initWithFrame:view.bounds device:device];
96
+ surface.opaque=NO; surface.backgroundColor=UIColor.clearColor;
97
+ surface.clearColor=MTLClearColorMake(0,0,0,0);
98
+ surface.userInteractionEnabled=NO; surface.accessibilityElementsHidden=YES;
99
+ surface.colorPixelFormat=MTLPixelFormatBGRA8Unorm;
100
+ surface.autoResizeDrawable=NO; surface.drawableSize=CGSizeMake(w,h);
101
+ surface.preferredFramesPerSecond=MIN(120,view.window.screen.maximumFramesPerSecond);
102
+ surface.paused=YES; surface.enableSetNeedsDisplay=YES;
103
+ surface.delegate=effect; effect.surface=surface;
104
+ [view addSubview:surface];
105
+ // Draw the identity frame before suppressing the original raster.
106
+ [surface draw]; view.image=nil;
107
+ if(mode==1) {
108
+ effect.pan=[[UIPanGestureRecognizer alloc] initWithTarget:effect action:@selector(drag:)];
109
+ effect.pan.delegate=effect; effect.pan.cancelsTouchesInView=NO;
110
+ view.userInteractionEnabled=YES; [view addGestureRecognizer:effect.pan];
111
+ }
112
+ return effect;
113
+ }
114
+ - (id<MTLTexture>)texture:(UIImage *)image device:(id<MTLDevice>)device width:(NSUInteger)w height:(NSUInteger)h {
115
+ if(!image.CGImage) return nil;
116
+ NSMutableData *data=[NSMutableData dataWithLength:w*h*4];
117
+ CGColorSpaceRef space=CGColorSpaceCreateDeviceRGB();
118
+ CGContextRef context=CGBitmapContextCreate(data.mutableBytes,w,h,8,w*4,space,kCGImageAlphaPremultipliedLast|kCGBitmapByteOrder32Big);
119
+ CGColorSpaceRelease(space);
120
+ if(!context) return nil;
121
+ CGContextTranslateCTM(context,0,h); CGContextScaleCTM(context,1,-1);
122
+ CGFloat iw=image.size.width,ih=image.size.height;
123
+ UIViewContentMode mode=self.image.contentMode;
124
+ CGFloat factor=mode==UIViewContentModeScaleAspectFill?MAX(w/iw,h/ih):MIN(w/iw,h/ih);
125
+ BOOL unscaled=mode==UIViewContentModeCenter || mode==UIViewContentModeTopLeft;
126
+ CGFloat dw=mode==UIViewContentModeScaleToFill?w:unscaled?iw*self.image.window.screen.scale:iw*factor;
127
+ CGFloat dh=mode==UIViewContentModeScaleToFill?h:unscaled?ih*self.image.window.screen.scale:ih*factor;
128
+ CGFloat left=mode==UIViewContentModeTopLeft?0:(w-dw)/2,top=mode==UIViewContentModeTopLeft?0:(h-dh)/2;
129
+ UIGraphicsPushContext(context); [image drawInRect:CGRectMake(left,top,dw,dh)]; UIGraphicsPopContext();
130
+ CGContextRelease(context);
131
+ MTLTextureDescriptor *descriptor=[MTLTextureDescriptor texture2DDescriptorWithPixelFormat:MTLPixelFormatRGBA8Unorm width:w height:h mipmapped:NO];
132
+ descriptor.usage=MTLTextureUsageShaderRead;
133
+ id<MTLTexture> texture=[device newTextureWithDescriptor:descriptor];
134
+ [texture replaceRegion:MTLRegionMake2D(0,0,w,h) mipmapLevel:0 withBytes:data.bytes bytesPerRow:w*4];
135
+ return texture;
136
+ }
137
+ - (void)play:(double)endpoint {
138
+ if(self.disposed || self.params.mode==1) return;
139
+ self.goal=endpoint; self.startProgress=self.progress; self.remaining=MAX(.001,self.duration*fabs(endpoint-self.progress));
140
+ self.anchor=CACurrentMediaTime(); self.playing=YES; self.settling=NO; self.paused=NO;
141
+ self.previousFrame=0; self.surface.enableSetNeedsDisplay=NO; self.surface.paused=NO;
142
+ }
143
+ - (void)pause {
144
+ if(self.disposed) return;
145
+ [self advance:CACurrentMediaTime()]; self.paused=YES;
146
+ self.surface.paused=YES; self.surface.enableSetNeedsDisplay=YES;
147
+ }
148
+ - (void)resume {
149
+ if(self.disposed || !self.paused) return;
150
+ if(self.params.mode==1) {
151
+ self.paused=NO;
152
+ if(self.settling && self.playing) {
153
+ self.settleStart=self.params.displacement;self.anchor=CACurrentMediaTime();
154
+ self.remaining=MIN(.6,self.duration);self.previousFrame=0;
155
+ self.surface.enableSetNeedsDisplay=NO;self.surface.paused=NO;
156
+ }
157
+ return;
158
+ }
159
+ [self play:self.goal];
160
+ }
161
+ - (void)seek:(double)value {
162
+ if(self.disposed || self.params.mode==1) return;
163
+ BOOL hold=self.paused; self.progress=value;
164
+ JoltImageParams p=self.params;p.progress=value;self.params=p;
165
+ [self.surface draw];
166
+ if(!hold) [self play:self.goal];
167
+ }
168
+ - (void)finish:(double)endpoint {
169
+ if(self.disposed || self.params.mode==1) return;
170
+ self.progress=endpoint; self.playing=NO;
171
+ self.surface.paused=YES;
172
+ if(self.listener) self.listener(YES,endpoint);
173
+ }
174
+ - (void)stop {
175
+ [self detach:YES];
176
+ }
177
+ - (void)stopPreservingImage {
178
+ [self detach:NO];
179
+ }
180
+ - (void)detach:(BOOL)restoreImage {
181
+ if(self.disposed) return;
182
+ self.disposed=YES; self.surface.paused=YES; self.surface.delegate=nil;
183
+ [self.surface removeFromSuperview]; self.surface=nil;
184
+ if(self.pan) { [self.image removeGestureRecognizer:self.pan];self.image.userInteractionEnabled=self.originalInteraction;self.pan=nil; }
185
+ if(restoreImage && !self.image.image) self.image.image=self.original;
186
+ self.sourceTexture=nil;self.destinationTexture=nil;self.original=nil;self.queue=nil;
187
+ self.textureBytes=0; self.listener=nil;
188
+ }
189
+ - (void)advance:(double)now {
190
+ if(!self.playing || self.paused || self.disposed) return;
191
+ double t=MIN(1,MAX(0,(now-self.anchor)/self.remaining));
192
+ JoltImageParams p=self.params;
193
+ if(self.settling) p.displacement=self.settleStart*(float)(exp(-8*t)*(1-t));
194
+ else {self.progress=self.startProgress+(self.goal-self.startProgress)*t;p.progress=self.progress;}
195
+ self.params=p;
196
+ if(t>=1) {
197
+ self.playing=NO;self.surface.paused=YES;self.surface.enableSetNeedsDisplay=YES;
198
+ if(!self.settling && self.listener) self.listener(YES,self.progress);
199
+ }
200
+ }
201
+ - (void)drawInMTKView:(MTKView *)view {
202
+ if(self.disposed) return;
203
+ double now=CACurrentMediaTime();
204
+ if(self.previousFrame>0 && self.playing) self.longestFrame=MAX(self.longestFrame,now-self.previousFrame);
205
+ self.previousFrame=now;self.frames++;
206
+ [self advance:now];
207
+ if(self.disposed) return;
208
+ MTLRenderPassDescriptor *pass=view.currentRenderPassDescriptor;
209
+ id<CAMetalDrawable> drawable=view.currentDrawable;
210
+ if(!pass || !drawable) return;
211
+ id<MTLCommandBuffer> command=[self.queue commandBuffer];
212
+ id<MTLRenderCommandEncoder> encoder=[command renderCommandEncoderWithDescriptor:pass];
213
+ [encoder setRenderPipelineState:self.pipeline];
214
+ JoltImageParams params=self.params;
215
+ [encoder setFragmentBytes:&params length:sizeof(params) atIndex:0];
216
+ [encoder setFragmentTexture:self.sourceTexture atIndex:0];
217
+ [encoder setFragmentTexture:self.destinationTexture atIndex:1];
218
+ [encoder drawPrimitives:MTLPrimitiveTypeTriangleStrip vertexStart:0 vertexCount:4];
219
+ [encoder endEncoding]; [command presentDrawable:drawable]; [command commit];
220
+ }
221
+ - (void)mtkView:(MTKView *)view drawableSizeWillChange:(CGSize)size {}
222
+ - (void)drag:(UIPanGestureRecognizer *)gesture {
223
+ if(self.disposed || self.paused) return;
224
+ CGFloat scale=self.image.window.screen.scale;
225
+ JoltImageParams p=self.params;
226
+ if(gesture.state==UIGestureRecognizerStateBegan) {
227
+ self.playing=NO;self.surface.paused=YES;self.surface.enableSetNeedsDisplay=YES;
228
+ CGPoint position=[gesture locationInView:self.image],translation=[gesture translationInView:self.image];
229
+ p.origin=(vector_float2){(float)((position.x-translation.x)*scale),(float)((position.y-translation.y)*scale)};
230
+ }
231
+ if(gesture.state==UIGestureRecognizerStateBegan || gesture.state==UIGestureRecognizerStateChanged) {
232
+ CGPoint delta=[gesture translationInView:self.image];
233
+ double length=hypot(delta.x,delta.y)*scale;
234
+ double factor=length==0?0:p.amplitude*tanh(length/MAX(p.amplitude,1))/length;
235
+ p.displacement=(vector_float2){(float)(delta.x*scale*factor),(float)(delta.y*scale*factor)};
236
+ self.params=p;[self.surface draw];
237
+ } else if(gesture.state==UIGestureRecognizerStateEnded || gesture.state==UIGestureRecognizerStateCancelled || gesture.state==UIGestureRecognizerStateFailed) {
238
+ self.settleStart=p.displacement;self.anchor=CACurrentMediaTime();self.remaining=MIN(.6,self.duration);
239
+ self.playing=YES;self.settling=YES;self.surface.enableSetNeedsDisplay=NO;self.surface.paused=NO;
240
+ }
241
+ }
242
+ @end
@@ -0,0 +1,23 @@
1
+ #import <UIKit/UIKit.h>
2
+ NS_ASSUME_NONNULL_BEGIN
3
+ @interface JoltPhotoZoom : NSObject
4
+ + (instancetype)attachImage:(UIImageView *)image viewport:(UIView *)viewport
5
+ minimum:(double)minimum maximum:(double)maximum doubleTap:(double)doubleTap
6
+ dismiss:(BOOL)dismiss reduced:(BOOL)reduced
7
+ backdrop:(nullable UIView *)backdrop chrome:(nullable UIView *)chrome
8
+ onDismiss:(void (^)(double,double,double,double,double))onDismiss
9
+ onSettled:(void (^)(double,double,double))onSettled;
10
+ @property(nonatomic, readonly) double x;
11
+ @property(nonatomic, readonly) double y;
12
+ @property(nonatomic, readonly) double scale;
13
+ @property(nonatomic, readonly) double dismissProgress;
14
+ @property(nonatomic, readonly) BOOL dismissPending;
15
+ - (void)zoom:(double)scale focusX:(double)x focusY:(double)y animated:(BOOL)animated;
16
+ - (void)reset:(BOOL)animated;
17
+ - (void)requestDismiss;
18
+ - (void)setReduced:(BOOL)reduced;
19
+ - (void)setActive:(BOOL)active;
20
+ - (void)layout;
21
+ - (void)stop;
22
+ @end
23
+ NS_ASSUME_NONNULL_END