@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.
- package/README.md +110 -3
- 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/index.d.ts +11 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -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 +2 -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/package.json +15 -3
- 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/TextReveal.java +180 -0
- package/platforms/android/proguard-rules.pro +6 -0
- package/src/android-plugin.d.ts +64 -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/index.ts +11 -1
- package/src/items.ts +140 -0
- package/src/keyboard.ts +132 -0
- package/src/label.ts +76 -0
- package/src/liquid.ts +2 -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/particles.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Application, Color, isIOS, type View } from '@nativescript/core'
|
|
2
2
|
import { toView } from './animate.js'
|
|
3
3
|
import type { Target } from './types.js'
|
|
4
|
+
import { reducedMotion } from './config.js'
|
|
4
5
|
|
|
5
6
|
export interface ConfettiOptions {
|
|
6
7
|
/** Approximate number of particles in the burst. Defaults to 80. */
|
|
@@ -78,8 +79,7 @@ function start(target: Target, settings: Settings, overlay: boolean): () => void
|
|
|
78
79
|
const view = toView(target)
|
|
79
80
|
const running = overlay ? bursts : emitters
|
|
80
81
|
running.get(view)?.()
|
|
81
|
-
if (!isIOS || settings.rate === 0 ||
|
|
82
|
-
return () => {}
|
|
82
|
+
if (!isIOS || settings.rate === 0 || reducedMotion()) return () => {}
|
|
83
83
|
return new Emitter(view, settings, overlay, running).stop
|
|
84
84
|
}
|
|
85
85
|
|
|
@@ -191,12 +191,12 @@ class Emitter {
|
|
|
191
191
|
}
|
|
192
192
|
|
|
193
193
|
private motionChanged = () => {
|
|
194
|
-
if (
|
|
194
|
+
if (reducedMotion()) this.stop()
|
|
195
195
|
}
|
|
196
196
|
|
|
197
197
|
private update() {
|
|
198
198
|
if (!this.view.isLoaded) return
|
|
199
|
-
if (
|
|
199
|
+
if (reducedMotion()) return this.stop()
|
|
200
200
|
const native = this.view.nativeViewProtected as UIView | undefined
|
|
201
201
|
const window = native?.window
|
|
202
202
|
if (this.layer && (!native || !window)) return this.stop()
|
package/src/path.ts
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { parsePath, type MotionPath } from './svg-path.js'
|
|
2
|
+
import { reducedMotion, timeScale } from './config.js'
|
|
3
|
+
import { createTrack, instantTrack, type Track } from './track.js'
|
|
4
|
+
import {
|
|
5
|
+
mergeTransitions,
|
|
6
|
+
resolveTransition,
|
|
7
|
+
type Transition,
|
|
8
|
+
type ResolvedTransition,
|
|
9
|
+
} from './transition.js'
|
|
10
|
+
import type { Keyframes, KeyframeProperty } from './types.js'
|
|
11
|
+
|
|
12
|
+
export type PathProperty = 'x' | 'y' | 'rotate'
|
|
13
|
+
export interface PreparedPath {
|
|
14
|
+
readonly properties: readonly PathProperty[]
|
|
15
|
+
readonly starts: ReadonlyMap<PathProperty, number>
|
|
16
|
+
readonly transition: ResolvedTransition
|
|
17
|
+
track(property: PathProperty, offset: number, delay: number): Track
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function preparePath(
|
|
21
|
+
keyframes: Keyframes,
|
|
22
|
+
base: Transition,
|
|
23
|
+
overrides: Partial<Record<KeyframeProperty | 'path', Transition>>,
|
|
24
|
+
): PreparedPath | undefined {
|
|
25
|
+
if (keyframes.orient !== undefined && typeof keyframes.orient !== 'boolean')
|
|
26
|
+
throw new TypeError('Path orient must be a boolean.')
|
|
27
|
+
if (keyframes.path === undefined) {
|
|
28
|
+
if (keyframes.orient !== undefined) throw new TypeError('orient requires a path.')
|
|
29
|
+
return undefined
|
|
30
|
+
}
|
|
31
|
+
if (keyframes.x !== undefined || keyframes.y !== undefined)
|
|
32
|
+
throw new TypeError('A path owns x and y; omit explicit x/y keyframes.')
|
|
33
|
+
if (keyframes.orient && keyframes.rotate !== undefined)
|
|
34
|
+
throw new TypeError('An oriented path owns rotate; omit explicit rotate keyframes.')
|
|
35
|
+
if (overrides.x || overrides.y || (keyframes.orient && overrides.rotate))
|
|
36
|
+
throw new TypeError('Use the path transition to time its coordinates together.')
|
|
37
|
+
const geometry = parsePath(keyframes.path)
|
|
38
|
+
const orient = keyframes.orient === true
|
|
39
|
+
const transition = resolveTransition(
|
|
40
|
+
overrides.path ? mergeTransitions(base, overrides.path) : base,
|
|
41
|
+
'opacity',
|
|
42
|
+
2,
|
|
43
|
+
)
|
|
44
|
+
if (
|
|
45
|
+
!Number.isFinite(transition.curve.type === 'tween' ? transition.curve.duration : 0) ||
|
|
46
|
+
!Number.isFinite(transition.delay)
|
|
47
|
+
)
|
|
48
|
+
throw new RangeError('Path duration and delay must be finite.')
|
|
49
|
+
const properties: readonly PathProperty[] = orient ? ['x', 'y', 'rotate'] : ['x', 'y']
|
|
50
|
+
const first = geometry.at(0)
|
|
51
|
+
let motion: ReturnType<typeof sample> | undefined
|
|
52
|
+
return {
|
|
53
|
+
properties,
|
|
54
|
+
starts: new Map(properties.map((property) => [property, first[property]])),
|
|
55
|
+
transition,
|
|
56
|
+
track(property, offset, requested) {
|
|
57
|
+
motion ??= sample(geometry, transition, orient)
|
|
58
|
+
const { progress, times, frames } = motion
|
|
59
|
+
const to = geometry.at(progress.to)[property] + offset
|
|
60
|
+
if (reducedMotion()) return instantTrack(property, to)
|
|
61
|
+
const delay = requested / timeScale()
|
|
62
|
+
const shift = delay - progress.delay
|
|
63
|
+
const pose = (time: number) =>
|
|
64
|
+
geometry.at(progress.valueAt(time - shift))[property] + offset
|
|
65
|
+
return {
|
|
66
|
+
...progress,
|
|
67
|
+
property,
|
|
68
|
+
delay,
|
|
69
|
+
end: progress.end + shift,
|
|
70
|
+
arrival: progress.arrival + shift,
|
|
71
|
+
times,
|
|
72
|
+
values: offset
|
|
73
|
+
? frames[property].map((value) => value + offset)
|
|
74
|
+
: frames[property],
|
|
75
|
+
to,
|
|
76
|
+
valueAt: pose,
|
|
77
|
+
velocityAt(time) {
|
|
78
|
+
if (time <= delay || time >= progress.end + shift) return 0
|
|
79
|
+
const position = progress.valueAt(time - shift)
|
|
80
|
+
if (position < 0 || position > 1) return 0
|
|
81
|
+
const epsilon = Math.min(1e-5, 0.01 / Math.max(1, geometry.length))
|
|
82
|
+
const a = Math.max(0, position - epsilon),
|
|
83
|
+
b = Math.min(1, position + epsilon)
|
|
84
|
+
return (
|
|
85
|
+
((geometry.at(b)[property] - geometry.at(a)[property]) / (b - a)) *
|
|
86
|
+
progress.velocityAt(time - shift)
|
|
87
|
+
)
|
|
88
|
+
},
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Sample scalar timing once, then project all coordinates through the same path. */
|
|
95
|
+
function sample(geometry: MotionPath, transition: ResolvedTransition, orient: boolean) {
|
|
96
|
+
const progress = createTrack('opacity', [0, 1], transition, 0)
|
|
97
|
+
const count = Math.max(
|
|
98
|
+
1,
|
|
99
|
+
Math.min(
|
|
100
|
+
6000,
|
|
101
|
+
Math.max(
|
|
102
|
+
Math.ceil((progress.duration / 1000) * 60),
|
|
103
|
+
Math.ceil(geometry.length / 2),
|
|
104
|
+
),
|
|
105
|
+
),
|
|
106
|
+
)
|
|
107
|
+
const times = Array.from({ length: count + 1 }, (_, i) => i / count)
|
|
108
|
+
const frames: Record<PathProperty, number[]> = { x: [], y: [], rotate: [] }
|
|
109
|
+
for (const time of times) {
|
|
110
|
+
const position =
|
|
111
|
+
time === 0
|
|
112
|
+
? 0
|
|
113
|
+
: time === 1
|
|
114
|
+
? 1
|
|
115
|
+
: progress.valueAt(progress.delay + time * progress.duration)
|
|
116
|
+
if (!Number.isFinite(position))
|
|
117
|
+
throw new RangeError('Path easing must return finite progress.')
|
|
118
|
+
const pose = geometry.at(position)
|
|
119
|
+
frames.x.push(pose.x)
|
|
120
|
+
frames.y.push(pose.y)
|
|
121
|
+
if (orient) frames.rotate.push(pose.rotate)
|
|
122
|
+
}
|
|
123
|
+
return { progress, times, frames }
|
|
124
|
+
}
|
package/src/playback-group.ts
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
arrivalOf,
|
|
3
|
+
arrivalTimingOf,
|
|
4
|
+
setArrival,
|
|
5
|
+
setArrivalTiming,
|
|
6
|
+
type ViewAnimation,
|
|
7
|
+
} from './engine.js'
|
|
2
8
|
import { getDriver } from './driver/index.js'
|
|
3
9
|
import { createClock, type PlaybackClock } from './playback.js'
|
|
4
10
|
import { validateProgress, type PlaybackAnimation } from './playback.js'
|
|
@@ -104,6 +110,33 @@ export function playbackGroup(children: readonly ViewAnimation[]): PlaybackAnima
|
|
|
104
110
|
then: (onfulfilled, onrejected) => finished.then(onfulfilled, onrejected),
|
|
105
111
|
}
|
|
106
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
|
+
})
|
|
107
140
|
ensureClock()
|
|
108
141
|
finishIfIdle()
|
|
109
142
|
return animation
|
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 {
|
|
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 =
|
|
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/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 (
|
|
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 ||
|
|
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) {
|
package/src/roll-diff.ts
ADDED
|
@@ -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
|
+
}
|