Skip to content

Commit cf905cc

Browse files
scottbedardantfu
andauthored
feat(useTransition): add support for custom interpolator functions (#5011)
Co-authored-by: Anthony Fu <[email protected]>
1 parent 61e1be0 commit cf905cc

File tree

5 files changed

+255
-78
lines changed

5 files changed

+255
-78
lines changed

packages/core/useTransition/demo.vue

Lines changed: 78 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,7 @@ const baseNumber = shallowRef(0)
88
99
const baseVector = shallowRef([0, 0])
1010
11-
function easeOutElastic(n: number) {
12-
return n === 0
13-
? 0
14-
: n === 1
15-
? 1
16-
: (2 ** (-10 * n)) * Math.sin((n * 10 - 0.75) * ((2 * Math.PI) / 3)) + 1
17-
}
11+
const baseWord = shallowRef('Hello')
1812
1913
const cubicBezierNumber = useTransition(baseNumber, {
2014
duration,
@@ -31,9 +25,82 @@ const vector = useTransition(baseVector, {
3125
transition: TransitionPresets.easeOutExpo,
3226
})
3327
28+
const word = useTransition(baseWord, {
29+
duration,
30+
interpolation: wordlerp,
31+
transition: TransitionPresets.easeInOutExpo,
32+
})
33+
34+
// Custom easing functions can control the progress of a transition,
35+
function easeOutElastic(n: number) {
36+
return n === 0
37+
? 0
38+
: n === 1
39+
? 1
40+
: (2 ** (-10 * n)) * Math.sin((n * 10 - 0.75) * ((2 * Math.PI) / 3)) + 1
41+
}
42+
43+
// and custom interpolation functions control the value of the transition, at
44+
// that progress. To demonstrate, let's say hello in a bunch of languages!
45+
const greetings = [
46+
'Ahoj',
47+
'Bok',
48+
'Ciao',
49+
'Czesc',
50+
'Hallo',
51+
'Hei',
52+
'Hej',
53+
'Hello',
54+
'Hola',
55+
'Merhaba',
56+
'Moi',
57+
'Namaste',
58+
'Ola',
59+
'Privet',
60+
'Salam',
61+
'Salut',
62+
'Sawasdee',
63+
'Shalom',
64+
'Yia',
65+
'Zdravo',
66+
]
67+
68+
function wordlerp(from: string, target: string, alpha: number) {
69+
from = from.padEnd(target.length)
70+
target = target.padEnd(from.length)
71+
72+
const arr = from.split('')
73+
const steps = [from]
74+
75+
while (arr.join('') !== target) {
76+
for (let i = 0; i < arr.length; i++) {
77+
const current = arr[i].charCodeAt(0)
78+
const next = target.charCodeAt(i)
79+
80+
if (current < next) {
81+
arr[i] = String.fromCharCode(current + 1)
82+
break
83+
}
84+
else if (current > next) {
85+
arr[i] = String.fromCharCode(current - 1)
86+
break
87+
}
88+
}
89+
steps.push(arr.join(''))
90+
}
91+
92+
if (alpha <= 0)
93+
return steps[0]
94+
if (alpha >= 1)
95+
return steps[steps.length - 1]
96+
return steps[Math.round(alpha * (steps.length - 1))]
97+
}
98+
3499
function toggle() {
35100
baseNumber.value = baseNumber.value === 100 ? 0 : 100
36101
baseVector.value = [rand(0, 100), rand(0, 100)]
102+
const arr = greetings.filter(word => word !== baseWord.value)
103+
baseWord.value = arr[rand(0, arr.length - 1)]
37104
}
38105
</script>
39106

@@ -72,6 +139,10 @@ function toggle() {
72139
<div class="sled" :style="{ left: `${vector[0]}%`, top: `${vector[1]}%` }" />
73140
</div>
74141
</div>
142+
143+
<p class="mt-2">
144+
Non-numeric value: <b>{{ word }}</b>
145+
</p>
75146
</div>
76147
</template>
77148

packages/core/useTransition/index.md

Lines changed: 28 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ Transition between values
88

99
## Usage
1010

11-
Define a numeric source value to follow, and when changed the output will transition to the new value. If the source changes while a transition is in progress, a new transition will begin from where the previous one was interrupted.
11+
Define a source value to follow, and when changed the output will transition to the new value. If the source changes while a transition is in progress, a new transition will begin from where the previous one was interrupted.
1212

1313
```ts
1414
import { TransitionPresets, useTransition } from '@vueuse/core'
@@ -18,32 +18,17 @@ const source = shallowRef(0)
1818

1919
const output = useTransition(source, {
2020
duration: 1000,
21-
transition: TransitionPresets.easeInOutCubic,
21+
easing: TransitionPresets.easeInOutCubic,
2222
})
2323
```
2424

25-
To synchronize transitions, use an array of numbers. As an example, here is how we could transition between colors.
26-
27-
```ts
28-
import { useTransition } from '@vueuse/core'
29-
// ---cut---
30-
const source = shallowRef([0, 0, 0])
31-
32-
const output = useTransition(source)
33-
34-
const color = computed(() => {
35-
const [r, g, b] = output.value
36-
return `rgb(${r}, ${g}, ${b})`
37-
})
38-
```
39-
40-
Transition easing can be customized using cubic bezier curves. Transitions defined this way work the same as [CSS easing functions](https://developer.mozilla.org/en-US/docs/Web/CSS/easing-function#easing_functions).
25+
Transition easing can be customized using [cubic bezier curves](https://developer.mozilla.org/en-US/docs/Web/CSS/easing-function/cubic-bezier#description).
4126

4227
```ts
4328
import { useTransition } from '@vueuse/core'
4429
// ---cut---
4530
useTransition(source, {
46-
transition: [0.75, 0, 0.25, 1],
31+
easing: [0.75, 0, 0.25, 1],
4732
})
4833
```
4934

@@ -75,7 +60,7 @@ The following transitions are available via the `TransitionPresets` constant.
7560
- [`easeOutBack`](https://cubic-bezier.com/#.34,1.56,.64,1)
7661
- [`easeInOutBack`](https://cubic-bezier.com/#.68,-.6,.32,1.6)
7762

78-
For more complex transitions, a custom function can be provided.
63+
For more complex easing, a custom function can be provided.
7964

8065
```ts
8166
import { useTransition } from '@vueuse/core'
@@ -89,7 +74,21 @@ function easeOutElastic(n) {
8974
}
9075

9176
useTransition(source, {
92-
transition: easeOutElastic,
77+
easing: easeOutElastic,
78+
})
79+
```
80+
81+
By default the `source` must be a number, or array of numbers. For more complex values, define a custom `interpolation` function. For example, the following would transition a Three.js rotation.
82+
83+
```ts
84+
import { useTransition } from '@vueuse/core'
85+
// ---cut---
86+
import { Quaternion } from 'three'
87+
88+
const source = ref(new Quaternion())
89+
90+
const output = useTransition(source, {
91+
interpolation: (q1, q2, t) => new Quaternion().slerpQuaternions(q1, q2, t)
9392
})
9493
```
9594

@@ -109,14 +108,17 @@ useTransition(source, {
109108
})
110109
```
111110

112-
To temporarily stop transitioning, define a boolean `disabled` property. Be aware, this is not the same a `duration` of `0`. Disabled transitions track the source value **_synchronously_**. They do not respect a `delay`, and do not fire `onStarted` or `onFinished` callbacks.
111+
To stop transitioning, define a boolean `disabled` property. Be aware, this is not the same a `duration` of `0`. Disabled transitions track the source value **_synchronously_**. They do not respect a `delay`, and do not fire `onStarted` or `onFinished` callbacks.
113112

114-
For more control, transitions can be executed manually by using `executeTransition`. This function returns a promise that resolves upon completion. Manual transitions can be cancelled by defining an `abort` function that returns a truthy value.
113+
For even more control, transitions can be executed manually via the `transition` function. This function returns a promise that resolves when the transition is complete. Manual transitions can be cancelled by defining an `abort` function that returns a truthy value.
115114

116115
```ts
117-
import { executeTransition } from '@vueuse/core'
116+
import { transition } from '@vueuse/core'
118117

119-
await executeTransition(source, from, to, {
120-
duration: 1000,
118+
await transition(source, from, to, {
119+
abort() {
120+
if (shouldAbort)
121+
return true
122+
}
121123
})
122124
```

packages/core/useTransition/index.test.ts

Lines changed: 64 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
11
import { promiseTimeout } from '@vueuse/shared'
22
import { describe, expect, it, vi } from 'vitest'
33
import { ref as deepRef, shallowRef } from 'vue'
4-
import { executeTransition, useTransition } from './index'
4+
import { executeTransition, transition, useTransition } from './index'
55

66
function expectBetween(val: number, floor: number, ceiling: number) {
77
expect(val).to.be.greaterThan(floor)
88
expect(val).to.be.lessThan(ceiling)
99
}
1010

11-
describe('executeTransition', () => {
11+
describe('transition', () => {
1212
it('transitions between numbers', async () => {
1313
const source = shallowRef(0)
1414

15-
const trans = executeTransition(source, 0, 1, { duration: 50 })
15+
const trans = transition(source, 0, 1, { duration: 50 })
1616

1717
await promiseTimeout(25)
1818

@@ -26,7 +26,7 @@ describe('executeTransition', () => {
2626
it('transitions between vectors', async () => {
2727
const source = deepRef([0, 0, 0])
2828

29-
const trans = executeTransition(source, [0, 1, 2], [1, 2, 3], { duration: 50 })
29+
const trans = transition(source, [0, 1, 2], [1, 2, 3], { duration: 50 })
3030

3131
await promiseTimeout(25)
3232

@@ -46,7 +46,7 @@ describe('executeTransition', () => {
4646

4747
const source = shallowRef(0)
4848

49-
const trans = executeTransition(source, 0, 1, {
49+
const trans = transition(source, 0, 1, {
5050
abort: () => abort,
5151
duration: 50,
5252
})
@@ -59,6 +59,20 @@ describe('executeTransition', () => {
5959

6060
expectBetween(source.value, 0, 1)
6161
})
62+
63+
it('supports deprecated `executeTransition` function', async () => {
64+
const source = shallowRef(0)
65+
66+
const trans = executeTransition(source, 0, 1, { duration: 50 })
67+
68+
await promiseTimeout(25)
69+
70+
expectBetween(source.value, 0.25, 0.75)
71+
72+
await trans
73+
74+
expect(source.value).toBe(1)
75+
})
6276
})
6377

6478
describe('useTransition', () => {
@@ -119,13 +133,13 @@ describe('useTransition', () => {
119133
// https://cubic-bezier.com/#0,2,0,1
120134
const easeOutBack = useTransition(source, {
121135
duration: 100,
122-
transition: [0, 2, 0, 1],
136+
easing: [0, 2, 0, 1],
123137
})
124138

125139
// https://cubic-bezier.com/#1,0,1,-1
126140
const easeInBack = useTransition(source, {
127141
duration: 100,
128-
transition: [1, 0, 1, -1],
142+
easing: [1, 0, 1, -1],
129143
})
130144

131145
source.value = 1
@@ -142,6 +156,26 @@ describe('useTransition', () => {
142156
it('supports custom easing functions', async () => {
143157
const source = shallowRef(0)
144158
const linear = vi.fn(n => n)
159+
const transition = useTransition(source, {
160+
duration: 100,
161+
easing: linear,
162+
})
163+
164+
expect(linear).not.toBeCalled()
165+
166+
source.value = 1
167+
168+
await promiseTimeout(50)
169+
expect(linear).toBeCalled()
170+
expectBetween(transition.value, 0, 1)
171+
172+
await promiseTimeout(100)
173+
expect(transition.value).toBe(1)
174+
})
175+
176+
it('supports deprecated `transition` option', async () => {
177+
const source = shallowRef(0)
178+
const linear = vi.fn(n => n * n)
145179
const transition = useTransition(source, {
146180
duration: 100,
147181
transition: linear,
@@ -164,7 +198,7 @@ describe('useTransition', () => {
164198
const easeInQuad = vi.fn(n => n * n)
165199
const transition = useTransition(source, {
166200
duration: 100,
167-
transition: easeInQuad,
201+
easing: easeInQuad,
168202
})
169203

170204
expect(easeInQuad).not.toBeCalled()
@@ -204,7 +238,7 @@ describe('useTransition', () => {
204238

205239
useTransition(source, {
206240
duration: 100,
207-
transition: easingFn,
241+
easing: easingFn,
208242
})
209243

210244
expect(first).not.toBeCalled()
@@ -338,4 +372,25 @@ describe('useTransition', () => {
338372

339373
expectBetween(transition.value, 0, 0.5)
340374
})
375+
376+
it('supports custom interpolation functions', async () => {
377+
const source = shallowRef('')
378+
const interpolation = vi.fn((_a, _b, n) => n < 1 / 2 ? 'foo' : 'bar')
379+
const transition = useTransition(source, {
380+
duration: 100,
381+
interpolation,
382+
})
383+
384+
source.value = 'test'
385+
386+
await promiseTimeout(25)
387+
expect(interpolation).toBeCalled()
388+
expect(transition.value).toBe('foo')
389+
390+
await promiseTimeout(50)
391+
expect(transition.value).toBe('bar')
392+
393+
await promiseTimeout(50)
394+
expect(transition.value).toBe('test')
395+
})
341396
})

0 commit comments

Comments
 (0)