@rafaelaugusto/jolt 0.1.0-beta.4 → 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 (201) hide show
  1. package/README.md +134 -4
  2. package/dist/animate.d.ts +7 -4
  3. package/dist/animate.d.ts.map +1 -1
  4. package/dist/animate.js +58 -13
  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/decoration.d.ts +18 -0
  11. package/dist/decoration.d.ts.map +1 -0
  12. package/dist/decoration.js +155 -0
  13. package/dist/decoration.js.map +1 -0
  14. package/dist/driver/android-color.d.ts +3 -0
  15. package/dist/driver/android-color.d.ts.map +1 -1
  16. package/dist/driver/android-color.js +20 -18
  17. package/dist/driver/android-color.js.map +1 -1
  18. package/dist/driver/android-decoration.d.ts +22 -0
  19. package/dist/driver/android-decoration.d.ts.map +1 -0
  20. package/dist/driver/android-decoration.js +139 -0
  21. package/dist/driver/android-decoration.js.map +1 -0
  22. package/dist/driver/android-playback.d.ts +13 -0
  23. package/dist/driver/android-playback.d.ts.map +1 -0
  24. package/dist/driver/android-playback.js +141 -0
  25. package/dist/driver/android-playback.js.map +1 -0
  26. package/dist/driver/android.d.ts.map +1 -1
  27. package/dist/driver/android.js +30 -36
  28. package/dist/driver/android.js.map +1 -1
  29. package/dist/driver/index.d.ts +9 -0
  30. package/dist/driver/index.d.ts.map +1 -1
  31. package/dist/driver/index.js.map +1 -1
  32. package/dist/driver/ios-color.d.ts +2 -0
  33. package/dist/driver/ios-color.d.ts.map +1 -1
  34. package/dist/driver/ios-color.js +19 -8
  35. package/dist/driver/ios-color.js.map +1 -1
  36. package/dist/driver/ios-decoration.d.ts +17 -0
  37. package/dist/driver/ios-decoration.d.ts.map +1 -0
  38. package/dist/driver/ios-decoration.js +271 -0
  39. package/dist/driver/ios-decoration.js.map +1 -0
  40. package/dist/driver/ios-playback.d.ts +10 -0
  41. package/dist/driver/ios-playback.d.ts.map +1 -0
  42. package/dist/driver/ios-playback.js +47 -0
  43. package/dist/driver/ios-playback.js.map +1 -0
  44. package/dist/driver/ios-shadow.d.ts +6 -0
  45. package/dist/driver/ios-shadow.d.ts.map +1 -0
  46. package/dist/driver/ios-shadow.js +52 -0
  47. package/dist/driver/ios-shadow.js.map +1 -0
  48. package/dist/driver/ios.d.ts.map +1 -1
  49. package/dist/driver/ios.js +76 -40
  50. package/dist/driver/ios.js.map +1 -1
  51. package/dist/engine.d.ts +64 -3
  52. package/dist/engine.d.ts.map +1 -1
  53. package/dist/engine.js +243 -19
  54. package/dist/engine.js.map +1 -1
  55. package/dist/haptics.d.ts +4 -1
  56. package/dist/haptics.d.ts.map +1 -1
  57. package/dist/haptics.js +27 -3
  58. package/dist/haptics.js.map +1 -1
  59. package/dist/index.d.ts +12 -1
  60. package/dist/index.d.ts.map +1 -1
  61. package/dist/index.js +5 -0
  62. package/dist/index.js.map +1 -1
  63. package/dist/items.d.ts +20 -0
  64. package/dist/items.d.ts.map +1 -0
  65. package/dist/items.js +109 -0
  66. package/dist/items.js.map +1 -0
  67. package/dist/keyboard.d.ts +17 -0
  68. package/dist/keyboard.d.ts.map +1 -0
  69. package/dist/keyboard.js +116 -0
  70. package/dist/keyboard.js.map +1 -0
  71. package/dist/label.d.ts +7 -0
  72. package/dist/label.d.ts.map +1 -0
  73. package/dist/label.js +60 -0
  74. package/dist/label.js.map +1 -0
  75. package/dist/liquid.d.ts.map +1 -1
  76. package/dist/liquid.js +2 -1
  77. package/dist/liquid.js.map +1 -1
  78. package/dist/loader.d.ts.map +1 -1
  79. package/dist/loader.js +2 -1
  80. package/dist/loader.js.map +1 -1
  81. package/dist/particles.d.ts.map +1 -1
  82. package/dist/particles.js +4 -3
  83. package/dist/particles.js.map +1 -1
  84. package/dist/path.d.ts +12 -0
  85. package/dist/path.d.ts.map +1 -0
  86. package/dist/path.js +90 -0
  87. package/dist/path.js.map +1 -0
  88. package/dist/playback-group.d.ts +6 -0
  89. package/dist/playback-group.d.ts.map +1 -0
  90. package/dist/playback-group.js +141 -0
  91. package/dist/playback-group.js.map +1 -0
  92. package/dist/playback.d.ts +25 -0
  93. package/dist/playback.d.ts.map +1 -0
  94. package/dist/playback.js +35 -0
  95. package/dist/playback.js.map +1 -0
  96. package/dist/press.d.ts.map +1 -1
  97. package/dist/press.js +3 -3
  98. package/dist/press.js.map +1 -1
  99. package/dist/properties.d.ts +2 -1
  100. package/dist/properties.d.ts.map +1 -1
  101. package/dist/properties.js +14 -1
  102. package/dist/properties.js.map +1 -1
  103. package/dist/reveal.d.ts.map +1 -1
  104. package/dist/reveal.js +3 -2
  105. package/dist/reveal.js.map +1 -1
  106. package/dist/roll-diff.d.ts +22 -0
  107. package/dist/roll-diff.d.ts.map +1 -0
  108. package/dist/roll-diff.js +36 -0
  109. package/dist/roll-diff.js.map +1 -0
  110. package/dist/roll.d.ts +29 -0
  111. package/dist/roll.d.ts.map +1 -0
  112. package/dist/roll.js +442 -0
  113. package/dist/roll.js.map +1 -0
  114. package/dist/scroll-linked.d.ts +10 -0
  115. package/dist/scroll-linked.d.ts.map +1 -0
  116. package/dist/scroll-linked.js +58 -0
  117. package/dist/scroll-linked.js.map +1 -0
  118. package/dist/sequence.d.ts +10 -5
  119. package/dist/sequence.d.ts.map +1 -1
  120. package/dist/sequence.js +208 -14
  121. package/dist/sequence.js.map +1 -1
  122. package/dist/shimmer.d.ts.map +1 -1
  123. package/dist/shimmer.js +2 -1
  124. package/dist/shimmer.js.map +1 -1
  125. package/dist/svg-path.d.ts +14 -0
  126. package/dist/svg-path.d.ts.map +1 -0
  127. package/dist/svg-path.js +281 -0
  128. package/dist/svg-path.js.map +1 -0
  129. package/dist/symbol-effect.d.ts.map +1 -1
  130. package/dist/symbol-effect.js +4 -3
  131. package/dist/symbol-effect.js.map +1 -1
  132. package/dist/text-layout.d.ts +8 -0
  133. package/dist/text-layout.d.ts.map +1 -1
  134. package/dist/text-layout.js +8 -2
  135. package/dist/text-layout.js.map +1 -1
  136. package/dist/text.d.ts +1 -1
  137. package/dist/text.d.ts.map +1 -1
  138. package/dist/text.js +60 -64
  139. package/dist/text.js.map +1 -1
  140. package/dist/track.d.ts +2 -0
  141. package/dist/track.d.ts.map +1 -1
  142. package/dist/track.js +72 -16
  143. package/dist/track.js.map +1 -1
  144. package/dist/transition.d.ts.map +1 -1
  145. package/dist/transition.js +4 -1
  146. package/dist/transition.js.map +1 -1
  147. package/dist/types.d.ts +22 -1
  148. package/dist/types.d.ts.map +1 -1
  149. package/dist/vue/index.d.ts +89 -0
  150. package/dist/vue/index.d.ts.map +1 -0
  151. package/dist/vue/index.js +134 -0
  152. package/dist/vue/index.js.map +1 -0
  153. package/package.json +15 -3
  154. package/platforms/android/java/org/rafaelaugusto/jolt/ColorTarget.java +2 -2
  155. package/platforms/android/java/org/rafaelaugusto/jolt/DecorationTarget.java +154 -0
  156. package/platforms/android/java/org/rafaelaugusto/jolt/KeyboardFollower.java +91 -0
  157. package/platforms/android/java/org/rafaelaugusto/jolt/NumberRoll.java +190 -0
  158. package/platforms/android/java/org/rafaelaugusto/jolt/TextReveal.java +180 -0
  159. package/platforms/android/proguard-rules.pro +7 -0
  160. package/src/android-plugin.d.ts +64 -0
  161. package/src/animate.ts +96 -33
  162. package/src/config.ts +46 -0
  163. package/src/decoration.ts +180 -0
  164. package/src/driver/android-color.ts +22 -20
  165. package/src/driver/android-decoration.ts +182 -0
  166. package/src/driver/android-playback.ts +157 -0
  167. package/src/driver/android.ts +57 -43
  168. package/src/driver/index.ts +15 -0
  169. package/src/driver/ios-color.ts +19 -10
  170. package/src/driver/ios-decoration.ts +332 -0
  171. package/src/driver/ios-playback.ts +51 -0
  172. package/src/driver/ios-shadow.ts +50 -0
  173. package/src/driver/ios.ts +87 -46
  174. package/src/engine.ts +267 -22
  175. package/src/haptics.ts +25 -2
  176. package/src/index.ts +12 -1
  177. package/src/items.ts +140 -0
  178. package/src/keyboard.ts +132 -0
  179. package/src/label.ts +76 -0
  180. package/src/liquid.ts +2 -1
  181. package/src/loader.ts +2 -1
  182. package/src/particles.ts +4 -4
  183. package/src/path.ts +124 -0
  184. package/src/playback-group.ts +143 -0
  185. package/src/playback.ts +62 -0
  186. package/src/press.ts +3 -5
  187. package/src/properties.ts +18 -2
  188. package/src/reveal.ts +3 -3
  189. package/src/roll-diff.ts +57 -0
  190. package/src/roll.ts +515 -0
  191. package/src/scroll-linked.ts +73 -0
  192. package/src/sequence.ts +204 -14
  193. package/src/shimmer.ts +2 -1
  194. package/src/svg-path.ts +355 -0
  195. package/src/symbol-effect.ts +4 -3
  196. package/src/text-layout.ts +14 -2
  197. package/src/text.ts +62 -81
  198. package/src/track.ts +98 -25
  199. package/src/transition.ts +4 -1
  200. package/src/types.ts +23 -1
  201. package/src/vue/index.ts +189 -0
@@ -0,0 +1,143 @@
1
+ import {
2
+ arrivalOf,
3
+ arrivalTimingOf,
4
+ setArrival,
5
+ setArrivalTiming,
6
+ type ViewAnimation,
7
+ } from './engine.js'
8
+ import { getDriver } from './driver/index.js'
9
+ import { createClock, type PlaybackClock } from './playback.js'
10
+ import { validateProgress, type PlaybackAnimation } from './playback.js'
11
+
12
+ /** Staggered views share a finite time range. Shorter children remain seekable
13
+ * until the whole group settles, unless another animation has taken ownership. */
14
+ export function playbackGroup(children: readonly ViewAnimation[]): PlaybackAnimation {
15
+ let paused = false
16
+ let settled = false
17
+ let finalProgress: number | undefined
18
+ let clock: PlaybackClock | undefined
19
+ let pendingProgress: number | undefined
20
+ const duration = () => Math.max(0, ...children.map((child) => child.duration))
21
+ const ensureClock = () => {
22
+ if (clock || settled || children.some((child) => !child.ready)) return
23
+ const driver = getDriver()
24
+ clock = driver.clock?.(duration()) ?? createClock(() => driver.now())
25
+ clock.seek(Math.max(0, ...children.map((child) => child.currentTime)))
26
+ if (paused) clock.pause()
27
+ }
28
+ const finishIfIdle = () => {
29
+ if (settled || paused || children.some((child) => !child.idle || child.paused)) return
30
+ settled = true
31
+ clock?.cancel()
32
+ for (const child of children) child.releaseGroup()
33
+ }
34
+ const seek = (value: number) => {
35
+ const total = duration()
36
+ validateProgress(value, total)
37
+ if (settled) return
38
+ if (children.some((child) => !child.ready)) {
39
+ pendingProgress = value
40
+ return
41
+ }
42
+ pendingProgress = undefined
43
+ ensureClock()
44
+ clock?.seek(total * value)
45
+ for (const child of children) child.seekTime(total * value)
46
+ finishIfIdle()
47
+ }
48
+ for (const child of children) {
49
+ child.onIdle(finishIfIdle)
50
+ child.onReady(() => {
51
+ ensureClock()
52
+ if (pendingProgress !== undefined && children.every((child) => child.ready))
53
+ seek(pendingProgress)
54
+ finishIfIdle()
55
+ })
56
+ }
57
+ const finished = Promise.all(children.map((child) => child.finished)).then(
58
+ (results) => {
59
+ finalProgress ??= results.every(Boolean)
60
+ ? 1
61
+ : duration()
62
+ ? Math.min(1, (clock?.time() ?? 0) / duration())
63
+ : 0
64
+ settled = true
65
+ clock?.cancel()
66
+ return results.every(Boolean)
67
+ },
68
+ )
69
+ const animation: PlaybackAnimation = {
70
+ finished,
71
+ get duration() {
72
+ return duration()
73
+ },
74
+ get paused() {
75
+ return paused
76
+ },
77
+ get progress() {
78
+ if (!Number.isFinite(duration()))
79
+ throw new RangeError('Progress requires a finite animation.')
80
+ if (pendingProgress !== undefined) return pendingProgress
81
+ return (
82
+ finalProgress ??
83
+ (settled ? 1 : duration() ? Math.min(1, (clock?.time() ?? 0) / duration()) : 0)
84
+ )
85
+ },
86
+ set progress(value) {
87
+ seek(value)
88
+ },
89
+ pause() {
90
+ if (!settled && !paused) {
91
+ paused = true
92
+ clock?.pause()
93
+ children.forEach((child) => child.pause())
94
+ }
95
+ },
96
+ play() {
97
+ if (!settled && paused) {
98
+ paused = false
99
+ clock?.play()
100
+ children.forEach((child) => child.play())
101
+ finishIfIdle()
102
+ }
103
+ },
104
+ stop() {
105
+ finalProgress = duration() ? Math.min(1, (clock?.time() ?? 0) / duration()) : 0
106
+ settled = true
107
+ clock?.cancel()
108
+ children.forEach((child) => child.stop())
109
+ },
110
+ then: (onfulfilled, onrejected) => finished.then(onfulfilled, onrejected),
111
+ }
112
+ setArrival(animation, Promise.all(children.map(arrivalOf)))
113
+ const listeners = new Set<() => void>()
114
+ const timings = children.map((child) => arrivalTimingOf(child)!)
115
+ const notify = () => {
116
+ for (const callback of [...listeners]) callback()
117
+ }
118
+ const unsubscribe = timings.map((timing) => timing.subscribe(notify))
119
+ setArrivalTiming(animation, {
120
+ read() {
121
+ const states = timings.map((timing) => timing.read())
122
+ return {
123
+ arrived: settled || states.every((state) => state.arrived),
124
+ paused: paused || states.some((state) => state.paused && !state.arrived),
125
+ remaining: states.some((state) => !state.arrived && state.remaining === undefined)
126
+ ? undefined
127
+ : Math.max(0, ...states.map((state) => (state.arrived ? 0 : state.remaining!))),
128
+ }
129
+ },
130
+ subscribe(callback) {
131
+ listeners.add(callback)
132
+ return () => listeners.delete(callback)
133
+ },
134
+ })
135
+ void finished.then(() => {
136
+ unsubscribe.forEach((off) => off())
137
+ notify()
138
+ listeners.clear()
139
+ })
140
+ ensureClock()
141
+ finishIfIdle()
142
+ return animation
143
+ }
@@ -0,0 +1,62 @@
1
+ import type { Animation } from './engine.js'
2
+
3
+ /** Controls returned by animate() and decay(). Effects expose the base Animation. */
4
+ export interface PlaybackAnimation extends Animation {
5
+ /** Pauses only the properties owned by this animation. */
6
+ pause(): void
7
+ /** Continues from the current position. Completed/stopped animations remain terminal. */
8
+ play(): void
9
+ readonly paused: boolean
10
+ /** Total milliseconds, including delays and repeats. Infinity for endless loops. */
11
+ readonly duration: number
12
+ /** Finite timeline position, from 0 to 1. Seeking preserves the playing/paused state. */
13
+ progress: number
14
+ }
15
+
16
+ export type PlaybackCommand = 'pause' | 'play' | 'seek'
17
+
18
+ export interface PlaybackClock {
19
+ time(): number
20
+ pause(): void
21
+ play(): void
22
+ seek(ms: number): void
23
+ cancel(): void
24
+ }
25
+
26
+ /** Read only on user commands; there is no frame loop in JavaScript. */
27
+ export function createClock(now: () => number, initial = 0): PlaybackClock {
28
+ let offset = initial
29
+ let anchor = now()
30
+ let paused = false
31
+ const time = () => offset + (paused ? 0 : now() - anchor)
32
+ return {
33
+ time,
34
+ pause() {
35
+ offset = time()
36
+ paused = true
37
+ },
38
+ play() {
39
+ if (paused) {
40
+ anchor = now()
41
+ paused = false
42
+ }
43
+ },
44
+ seek(ms) {
45
+ offset = ms
46
+ anchor = now()
47
+ },
48
+ cancel() {
49
+ offset = time()
50
+ paused = true
51
+ },
52
+ }
53
+ }
54
+
55
+ export function validateProgress(value: number, duration: number) {
56
+ if (!Number.isFinite(value) || value < 0 || value > 1)
57
+ throw new RangeError('Animation progress must be between 0 and 1.')
58
+ if (!Number.isFinite(duration))
59
+ throw new RangeError(
60
+ 'Progress requires a finite animation; endless loops can pause and play.',
61
+ )
62
+ }
package/src/press.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { Color, type EventData, type TouchGestureEventData } from '@nativescript/core'
2
2
  import { isColorProperty } from './color.js'
3
- import { animate, toView } from './animate.js'
4
- import { isProperty, readValue, type Property } from './properties.js'
3
+ import { animate, startingValues, toView } from './animate.js'
4
+ import { readValue } from './properties.js'
5
5
  import type { SpringName, Transition } from './transition.js'
6
6
  import type { AnimateOptions, Keyframes, Target } from './types.js'
7
7
 
@@ -23,9 +23,7 @@ export function press(
23
23
  options?: AnimateOptions | SpringName,
24
24
  ): () => void {
25
25
  const view = toView(target)
26
- const properties = Object.keys(pressed).flatMap((key): Property[] =>
27
- key === 'scale' ? ['scaleX', 'scaleY'] : isProperty(key) ? [key] : [],
28
- )
26
+ const properties = [...startingValues(pressed, options).keys()]
29
27
  let rest: Keyframes | undefined
30
28
 
31
29
  const onTouch = (event: EventData) => {
package/src/properties.ts CHANGED
@@ -1,9 +1,15 @@
1
1
  import { Color, type View } from '@nativescript/core'
2
+ import {
3
+ isDecorationProperty,
4
+ readDecoration,
5
+ writeDecoration,
6
+ type DecorationProperty,
7
+ } from './decoration.js'
2
8
  import { isColorProperty, readColor, type ColorProperty } from './color.js'
3
9
 
4
10
  export type NumericProperty =
5
11
  'opacity' | 'x' | 'y' | 'scaleX' | 'scaleY' | 'rotate' | 'rotateX' | 'rotateY'
6
- export type Property = NumericProperty | ColorProperty
12
+ export type Property = NumericProperty | ColorProperty | DecorationProperty
7
13
 
8
14
  const viewKeys = {
9
15
  opacity: 'opacity',
@@ -29,6 +35,14 @@ const defaults: Record<NumericProperty, number> = {
29
35
 
30
36
  /** How close to its target a spring or decay has to be to count as settled. */
31
37
  export const precision: Record<Property, number> = {
38
+ borderRadius: 0.1,
39
+ borderWidth: 0.1,
40
+ shadowOpacity: 0.002,
41
+ shadowRadius: 0.1,
42
+ shadowOffsetX: 0.1,
43
+ shadowOffsetY: 0.1,
44
+ elevation: 0.1,
45
+ translationZ: 0.1,
32
46
  backgroundColor: 0.002,
33
47
  borderColor: 0.002,
34
48
  color: 0.002,
@@ -43,11 +57,12 @@ export const precision: Record<Property, number> = {
43
57
  }
44
58
 
45
59
  export function isProperty(key: string): key is Property {
46
- return key in viewKeys || isColorProperty(key)
60
+ return Object.hasOwn(viewKeys, key) || isColorProperty(key) || isDecorationProperty(key)
47
61
  }
48
62
 
49
63
  export function readValue(view: View, property: Property): number {
50
64
  if (isColorProperty(property)) return readColor(view, property)
65
+ if (isDecorationProperty(property)) return readDecoration(view, property)
51
66
  const value = view[viewKeys[property]]
52
67
  return typeof value === 'number' && !Number.isNaN(value) ? value : defaults[property]
53
68
  }
@@ -57,5 +72,6 @@ export function writeValue(view: View, property: Property, value: number) {
57
72
  view[property] = new Color(value)
58
73
  return
59
74
  }
75
+ if (isDecorationProperty(property)) return writeDecoration(view, property, value)
60
76
  view[viewKeys[property]] = value
61
77
  }
package/src/reveal.ts CHANGED
@@ -3,6 +3,7 @@ import { toView } from './animate.js'
3
3
  import { resolveEasing } from './easing.js'
4
4
  import type { Animation } from './engine.js'
5
5
  import type { Target } from './types.js'
6
+ import { reducedMotion } from './config.js'
6
7
 
7
8
  export interface RevealOptions {
8
9
  /** Circle origin in DIPs from the target's top-left corner. Defaults to its center. */
@@ -243,14 +244,13 @@ class CircularAnimation implements Animation {
243
244
  }
244
245
 
245
246
  private motionChanged = () => {
246
- if (UIAccessibilityIsReduceMotionEnabled()) this.end(true)
247
+ if (reducedMotion()) this.end(true)
247
248
  }
248
249
 
249
250
  private update() {
250
251
  if (!this.opening && !this.inherited && this.view.visibility !== 'visible')
251
252
  return this.end(true, true)
252
- if (this.duration === 0 || UIAccessibilityIsReduceMotionEnabled())
253
- return this.end(true)
253
+ if (this.duration === 0 || reducedMotion()) return this.end(true)
254
254
  if (this.waitingForLoad || !this.view.isLoaded) return
255
255
  const native = this.view.nativeViewProtected as MaskedView | undefined
256
256
  if (this.shape) {
@@ -0,0 +1,57 @@
1
+ export interface Grapheme {
2
+ text: string
3
+ /** UTF-16 offsets, the units NSString locations and Java char indices use. */
4
+ start: number
5
+ end: number
6
+ }
7
+
8
+ export interface RollChange {
9
+ /** The grapheme that rolls in, placed in the new text. */
10
+ next: Grapheme
11
+ /** The grapheme it replaces at the same position from the end, when there is one. */
12
+ previous: Grapheme | undefined
13
+ }
14
+
15
+ export interface RollDiff {
16
+ /** Positions that differ, from the last one backwards. */
17
+ changes: RollChange[]
18
+ /** Old graphemes to the left of the new text's first one; they roll out alone. */
19
+ removed: Grapheme[]
20
+ }
21
+
22
+ const segmenter =
23
+ typeof Intl !== 'undefined' && 'Segmenter' in Intl
24
+ ? new Intl.Segmenter(undefined, { granularity: 'grapheme' })
25
+ : undefined
26
+
27
+ export function graphemes(text: string): Grapheme[] {
28
+ if (segmenter)
29
+ return [...segmenter.segment(text)].map(({ segment, index }) => ({
30
+ text: segment,
31
+ start: index,
32
+ end: index + segment.length,
33
+ }))
34
+ const pieces: Grapheme[] = []
35
+ let offset = 0
36
+ for (const character of text) {
37
+ pieces.push({ text: character, start: offset, end: offset + character.length })
38
+ offset += character.length
39
+ }
40
+ return pieces
41
+ }
42
+
43
+ /** Aligns both texts on their last grapheme, so the units digit stays put when the count grows. */
44
+ export function diffFromEnd(previous: string, next: string): RollDiff {
45
+ const before = graphemes(previous)
46
+ const after = graphemes(next)
47
+ const changes: RollChange[] = []
48
+ const removed: Grapheme[] = []
49
+ const count = Math.max(before.length, after.length)
50
+ for (let offset = 1; offset <= count; offset++) {
51
+ const old = before[before.length - offset]
52
+ const piece = after[after.length - offset]
53
+ if (!piece) removed.push(old!)
54
+ else if (!old || old.text !== piece.text) changes.push({ next: piece, previous: old })
55
+ }
56
+ return { changes, removed }
57
+ }