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

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 (150) hide show
  1. package/README.md +110 -3
  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/index.d.ts +11 -1
  32. package/dist/index.d.ts.map +1 -1
  33. package/dist/index.js +5 -0
  34. package/dist/index.js.map +1 -1
  35. package/dist/items.d.ts +20 -0
  36. package/dist/items.d.ts.map +1 -0
  37. package/dist/items.js +109 -0
  38. package/dist/items.js.map +1 -0
  39. package/dist/keyboard.d.ts +17 -0
  40. package/dist/keyboard.d.ts.map +1 -0
  41. package/dist/keyboard.js +116 -0
  42. package/dist/keyboard.js.map +1 -0
  43. package/dist/label.d.ts +7 -0
  44. package/dist/label.d.ts.map +1 -0
  45. package/dist/label.js +60 -0
  46. package/dist/label.js.map +1 -0
  47. package/dist/liquid.d.ts.map +1 -1
  48. package/dist/liquid.js +2 -1
  49. package/dist/liquid.js.map +1 -1
  50. package/dist/loader.d.ts.map +1 -1
  51. package/dist/loader.js +2 -1
  52. package/dist/loader.js.map +1 -1
  53. package/dist/particles.d.ts.map +1 -1
  54. package/dist/particles.js +4 -3
  55. package/dist/particles.js.map +1 -1
  56. package/dist/path.d.ts +12 -0
  57. package/dist/path.d.ts.map +1 -0
  58. package/dist/path.js +90 -0
  59. package/dist/path.js.map +1 -0
  60. package/dist/playback-group.d.ts.map +1 -1
  61. package/dist/playback-group.js +29 -1
  62. package/dist/playback-group.js.map +1 -1
  63. package/dist/press.d.ts.map +1 -1
  64. package/dist/press.js +3 -3
  65. package/dist/press.js.map +1 -1
  66. package/dist/reveal.d.ts.map +1 -1
  67. package/dist/reveal.js +3 -2
  68. package/dist/reveal.js.map +1 -1
  69. package/dist/roll-diff.d.ts +22 -0
  70. package/dist/roll-diff.d.ts.map +1 -0
  71. package/dist/roll-diff.js +36 -0
  72. package/dist/roll-diff.js.map +1 -0
  73. package/dist/roll.d.ts +29 -0
  74. package/dist/roll.d.ts.map +1 -0
  75. package/dist/roll.js +442 -0
  76. package/dist/roll.js.map +1 -0
  77. package/dist/scroll-linked.d.ts +10 -0
  78. package/dist/scroll-linked.d.ts.map +1 -0
  79. package/dist/scroll-linked.js +58 -0
  80. package/dist/scroll-linked.js.map +1 -0
  81. package/dist/sequence.d.ts +10 -5
  82. package/dist/sequence.d.ts.map +1 -1
  83. package/dist/sequence.js +202 -14
  84. package/dist/sequence.js.map +1 -1
  85. package/dist/shimmer.d.ts.map +1 -1
  86. package/dist/shimmer.js +2 -1
  87. package/dist/shimmer.js.map +1 -1
  88. package/dist/svg-path.d.ts +14 -0
  89. package/dist/svg-path.d.ts.map +1 -0
  90. package/dist/svg-path.js +281 -0
  91. package/dist/svg-path.js.map +1 -0
  92. package/dist/symbol-effect.d.ts.map +1 -1
  93. package/dist/symbol-effect.js +4 -3
  94. package/dist/symbol-effect.js.map +1 -1
  95. package/dist/text-layout.d.ts +8 -0
  96. package/dist/text-layout.d.ts.map +1 -1
  97. package/dist/text-layout.js +8 -2
  98. package/dist/text-layout.js.map +1 -1
  99. package/dist/text.d.ts +1 -1
  100. package/dist/text.d.ts.map +1 -1
  101. package/dist/text.js +60 -64
  102. package/dist/text.js.map +1 -1
  103. package/dist/track.d.ts +2 -0
  104. package/dist/track.d.ts.map +1 -1
  105. package/dist/track.js +62 -15
  106. package/dist/track.js.map +1 -1
  107. package/dist/types.d.ts +7 -1
  108. package/dist/types.d.ts.map +1 -1
  109. package/dist/vue/index.d.ts +89 -0
  110. package/dist/vue/index.d.ts.map +1 -0
  111. package/dist/vue/index.js +134 -0
  112. package/dist/vue/index.js.map +1 -0
  113. package/package.json +15 -3
  114. package/platforms/android/java/org/rafaelaugusto/jolt/KeyboardFollower.java +91 -0
  115. package/platforms/android/java/org/rafaelaugusto/jolt/NumberRoll.java +190 -0
  116. package/platforms/android/java/org/rafaelaugusto/jolt/TextReveal.java +180 -0
  117. package/platforms/android/proguard-rules.pro +6 -0
  118. package/src/android-plugin.d.ts +64 -0
  119. package/src/animate.ts +80 -30
  120. package/src/config.ts +46 -0
  121. package/src/driver/android.ts +14 -0
  122. package/src/driver/index.ts +2 -0
  123. package/src/driver/ios-color.ts +1 -1
  124. package/src/driver/ios-decoration.ts +1 -1
  125. package/src/driver/ios-playback.ts +15 -1
  126. package/src/driver/ios.ts +2 -0
  127. package/src/engine.ts +47 -2
  128. package/src/index.ts +11 -1
  129. package/src/items.ts +140 -0
  130. package/src/keyboard.ts +132 -0
  131. package/src/label.ts +76 -0
  132. package/src/liquid.ts +2 -1
  133. package/src/loader.ts +2 -1
  134. package/src/particles.ts +4 -4
  135. package/src/path.ts +124 -0
  136. package/src/playback-group.ts +34 -1
  137. package/src/press.ts +3 -5
  138. package/src/reveal.ts +3 -3
  139. package/src/roll-diff.ts +57 -0
  140. package/src/roll.ts +515 -0
  141. package/src/scroll-linked.ts +73 -0
  142. package/src/sequence.ts +199 -14
  143. package/src/shimmer.ts +2 -1
  144. package/src/svg-path.ts +355 -0
  145. package/src/symbol-effect.ts +4 -3
  146. package/src/text-layout.ts +14 -2
  147. package/src/text.ts +62 -81
  148. package/src/track.ts +89 -24
  149. package/src/types.ts +8 -1
  150. package/src/vue/index.ts +189 -0
@@ -0,0 +1,190 @@
1
+ package org.rafaelaugusto.jolt;
2
+
3
+ import android.animation.Animator;
4
+ import android.animation.AnimatorListenerAdapter;
5
+ import android.animation.ValueAnimator;
6
+ import android.graphics.Canvas;
7
+ import android.graphics.Paint;
8
+ import android.text.SpannableStringBuilder;
9
+ import android.text.Spanned;
10
+ import android.text.method.TransformationMethod;
11
+ import android.text.style.ReplacementSpan;
12
+ import android.view.animation.LinearInterpolator;
13
+ import android.view.animation.PathInterpolator;
14
+ import android.widget.TextView;
15
+
16
+ /** Rolls the changed positions of a TextView with one native animator. */
17
+ public final class NumberRoll {
18
+ public interface Callback {
19
+ void onEnd(boolean finished);
20
+ }
21
+
22
+ private static final int MAX_POSITIONS = 256;
23
+ private final TextView view;
24
+ private final CharSequence original;
25
+ private final CharSequence shown;
26
+ private final TransformationMethod method;
27
+ private final DigitSpan[] spans;
28
+ private final int[] starts;
29
+ private final int[] ends;
30
+ private final float[] times;
31
+ private final float[] values;
32
+ private final long duration;
33
+ private final long delay;
34
+ private final long stagger;
35
+ private final long fade;
36
+ private final boolean tabular;
37
+ private final Callback callback;
38
+ private final PathInterpolator ease = new PathInterpolator(0f, 0f, 0.58f, 1f);
39
+ private ValueAnimator animator;
40
+ private CharSequence current;
41
+ private boolean removedMethod;
42
+ private boolean ended;
43
+
44
+ // Positions are ordered from the end of the text; each one starts `stagger` after the previous.
45
+ public NumberRoll(TextView view, int[] starts, int[] ends, String[] previous, int direction,
46
+ float[] times, float[] values, long duration, long delay, long stagger, float shift,
47
+ long fade, boolean tabular, Callback callback) {
48
+ this.view = view;
49
+ this.original = view.getText();
50
+ // NativeScript installs a TransformationMethod that rebuilds the displayed text from its
51
+ // own model, discarding spans set from here. Work on what is displayed and remove it while rolling.
52
+ this.method = view.getTransformationMethod();
53
+ CharSequence displayed = method != null && original != null ? method.getTransformation(original, view) : original;
54
+ this.shown = displayed != null ? displayed : "";
55
+ this.starts = starts;
56
+ this.ends = ends;
57
+ this.times = times;
58
+ this.values = values;
59
+ this.duration = duration;
60
+ this.delay = delay;
61
+ this.stagger = stagger;
62
+ this.fade = fade;
63
+ this.tabular = tabular;
64
+ this.callback = callback;
65
+ spans = new DigitSpan[starts.length];
66
+ for (int i = 0; i < starts.length; i++) {
67
+ String old = i < previous.length && previous[i] != null ? previous[i] : "";
68
+ spans[i] = new DigitSpan(old, direction, shift, values.length > 0 ? values[0] : shift);
69
+ }
70
+ }
71
+
72
+ public boolean isSupported() {
73
+ if (original == null || starts.length == 0 || starts.length > MAX_POSITIONS) return false;
74
+ if (ends.length != starts.length || times.length < 2 || times.length != values.length) return false;
75
+ int length = shown.length();
76
+ for (int i = 0; i < starts.length; i++) {
77
+ if (starts[i] < 0 || ends[i] > length || starts[i] >= ends[i]) return false;
78
+ }
79
+ return true;
80
+ }
81
+
82
+ public void start() {
83
+ if (ended || animator != null) return;
84
+ if (tabular) view.setFontFeatureSettings("tnum");
85
+ SpannableStringBuilder spannable = new SpannableStringBuilder(shown);
86
+ for (int i = 0; i < spans.length; i++) {
87
+ spannable.setSpan(spans[i], starts[i], ends[i], Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
88
+ }
89
+ if (method != null) {
90
+ removedMethod = true;
91
+ view.setTransformationMethod(null);
92
+ }
93
+ view.setText(spannable);
94
+ current = view.getText();
95
+ final long total = delay + duration + stagger * (spans.length - 1);
96
+ animator = ValueAnimator.ofFloat(0f, 1f);
97
+ animator.setDuration(Math.max(total, 1));
98
+ animator.setInterpolator(new LinearInterpolator());
99
+ animator.addUpdateListener(animation -> update(animation.getAnimatedFraction() * total));
100
+ animator.addListener(new AnimatorListenerAdapter() {
101
+ @Override
102
+ public void onAnimationEnd(Animator animation) {
103
+ finish(true);
104
+ }
105
+ });
106
+ animator.start();
107
+ }
108
+
109
+ public void cancel() {
110
+ finish(false);
111
+ if (animator != null) animator.cancel();
112
+ }
113
+
114
+ private void update(float elapsed) {
115
+ for (int i = 0; i < spans.length; i++) {
116
+ float local = elapsed - delay - stagger * i;
117
+ spans[i].set(sample(local), alpha(local));
118
+ }
119
+ view.invalidate();
120
+ }
121
+
122
+ private float sample(float local) {
123
+ if (local <= 0) return values[0];
124
+ if (local >= duration) return values[values.length - 1];
125
+ float t = local / duration;
126
+ int i = 1;
127
+ while (i < times.length - 1 && times[i] < t) i++;
128
+ float span = times[i] - times[i - 1];
129
+ float fraction = span > 0 ? (t - times[i - 1]) / span : 1f;
130
+ return values[i - 1] + (values[i] - values[i - 1]) * fraction;
131
+ }
132
+
133
+ private float alpha(float local) {
134
+ if (local <= 0) return 0f;
135
+ if (fade <= 0 || local >= fade) return 1f;
136
+ return ease.getInterpolation(local / fade);
137
+ }
138
+
139
+ private void finish(boolean finished) {
140
+ if (ended) return;
141
+ ended = true;
142
+ if (current != null && view.getText() == current) view.setText(original);
143
+ if (removedMethod) view.setTransformationMethod(method);
144
+ callback.onEnd(finished);
145
+ }
146
+
147
+ /** Draws the new glyphs sliding in over the old ones sliding out, clipped to the line. */
148
+ static final class DigitSpan extends ReplacementSpan {
149
+ private final String previous;
150
+ private final int direction;
151
+ private final float shift;
152
+ private float offset;
153
+ private float alpha;
154
+
155
+ DigitSpan(String previous, int direction, float shift, float offset) {
156
+ this.previous = previous;
157
+ this.direction = direction;
158
+ this.shift = shift;
159
+ this.offset = offset;
160
+ }
161
+
162
+ void set(float offset, float alpha) {
163
+ this.offset = offset;
164
+ this.alpha = alpha;
165
+ }
166
+
167
+ @Override
168
+ public int getSize(Paint paint, CharSequence text, int start, int end, Paint.FontMetricsInt fm) {
169
+ if (fm != null) paint.getFontMetricsInt(fm);
170
+ return Math.round(Math.max(paint.measureText(text, start, end), paint.measureText(previous)));
171
+ }
172
+
173
+ @Override
174
+ public void draw(Canvas canvas, CharSequence text, int start, int end, float x, int top, int y,
175
+ int bottom, Paint paint) {
176
+ int base = paint.getAlpha();
177
+ float width = getSize(paint, text, start, end, null);
178
+ canvas.save();
179
+ canvas.clipRect(x, top, x + width, bottom);
180
+ paint.setAlpha(Math.round(base * alpha));
181
+ canvas.drawText(text, start, end, x, y + direction * offset, paint);
182
+ if (!previous.isEmpty()) {
183
+ paint.setAlpha(Math.round(base * (1f - alpha)));
184
+ canvas.drawText(previous, x, y + direction * (offset - shift), paint);
185
+ }
186
+ paint.setAlpha(base);
187
+ canvas.restore();
188
+ }
189
+ }
190
+ }
@@ -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,9 @@
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$* { *; }
@@ -0,0 +1,64 @@
1
+ declare namespace org.rafaelaugusto.jolt {
2
+ class KeyboardFollower {
3
+ constructor(
4
+ view: android.view.View,
5
+ offset: number,
6
+ listener: KeyboardFollower.Listener,
7
+ )
8
+ attach(): void
9
+ detach(): void
10
+ }
11
+ namespace KeyboardFollower {
12
+ class Listener {
13
+ constructor(implementation: {
14
+ onStart(): void
15
+ onSettled(translation: number): void
16
+ })
17
+ }
18
+ }
19
+ class TextReveal {
20
+ constructor(
21
+ view: android.widget.TextView,
22
+ byWord: boolean,
23
+ duration: number,
24
+ stagger: number,
25
+ delay: number,
26
+ offset: number,
27
+ callback: TextReveal.Callback,
28
+ )
29
+ isSupported(): boolean
30
+ start(): void
31
+ cancel(): void
32
+ }
33
+ namespace TextReveal {
34
+ class Callback {
35
+ constructor(implementation: { onEnd(finished: boolean): void })
36
+ }
37
+ }
38
+ class NumberRoll {
39
+ constructor(
40
+ view: android.widget.TextView,
41
+ starts: androidNative.Array<number>,
42
+ ends: androidNative.Array<number>,
43
+ previous: androidNative.Array<string>,
44
+ direction: number,
45
+ times: androidNative.Array<number>,
46
+ values: androidNative.Array<number>,
47
+ duration: number,
48
+ delay: number,
49
+ stagger: number,
50
+ shift: number,
51
+ fade: number,
52
+ tabular: boolean,
53
+ callback: NumberRoll.Callback,
54
+ )
55
+ isSupported(): boolean
56
+ start(): void
57
+ cancel(): void
58
+ }
59
+ namespace NumberRoll {
60
+ class Callback {
61
+ constructor(implementation: { onEnd(finished: boolean): void })
62
+ }
63
+ }
64
+ }
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
+ }