From a7b81b7d5e0d1165245c334095f459f0743070ae Mon Sep 17 00:00:00 2001 From: Scott Bedard Date: Mon, 15 Mar 2021 06:52:50 -0500 Subject: [PATCH 01/84] feat(useTransition): support for vectors (#376) --- packages/core/useTransition/demo.vue | 63 ++++++++++++++++++----- packages/core/useTransition/index.md | 18 +++---- packages/core/useTransition/index.test.ts | 34 ++++++++++++ packages/core/useTransition/index.ts | 26 ++++++---- packages/shared/utils/is.ts | 5 ++ 5 files changed, 110 insertions(+), 36 deletions(-) diff --git a/packages/core/useTransition/demo.vue b/packages/core/useTransition/demo.vue index 3655e73aad8..a09736eb44b 100644 --- a/packages/core/useTransition/demo.vue +++ b/packages/core/useTransition/demo.vue @@ -1,9 +1,14 @@ @@ -33,15 +44,11 @@ const toggle = () => { Transition -

- Base number: {{ baseNumber }} -

-

Cubic bezier curve: {{ cubicBezierNumber.toFixed(2) }}

-
+
@@ -51,22 +58,29 @@ const toggle = () => { Custom function: {{ customFnNumber.toFixed(2) }}

-
+
+ +

+ Vector: [{{ vector[0].toFixed(2) }}, {{ vector[1].toFixed(2) }}] +

+ +
+
+
+
+
diff --git a/packages/core/useTransition/index.md b/packages/core/useTransition/index.md index fe5a616f0f8..683b892083d 100644 --- a/packages/core/useTransition/index.md +++ b/packages/core/useTransition/index.md @@ -11,7 +11,7 @@ Transition between values ```js import { useTransition, TransitionPresets } from '@vueuse/core' -useTransition(baseNumber, { +useTransition(source, { duration: 1000, transition: TransitionPresets.easeInOutCubic, }) @@ -46,8 +46,7 @@ The following transitions are available via the `TransitionPresets` constant. Custom transitions can be defined 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). ```js -useTransition(baseNumber, { - duration: 1000, +useTransition(source, { transition: [0.75, 0, 0.25, 1], }) ``` @@ -63,8 +62,7 @@ const easeOutElastic = (n) => { : (2 ** (-10 * n)) * Math.sin((n * 10 - 0.75) * ((2 * Math.PI) / 3)) + 1 } -useTransition(baseNumber, { - duration: 1000, +useTransition(source, { transition: easeOutElastic, }) ``` @@ -72,9 +70,7 @@ useTransition(baseNumber, { To choreograph behavior around a transition, define `onStarted` or `onFinished` callbacks. ```js -useTransition(baseNumber, { - duration: 1000, - transition: easeOutElastic, +useTransition(source, { onStarted() { // called after the transition starts }, @@ -119,10 +115,10 @@ export declare const TransitionPresets: Record * @param source * @param options */ -export declare function useTransition( - source: Ref, +export declare function useTransition>( + source: T, options?: TransitionOptions -): Ref +): ComputedRef> export {} ``` diff --git a/packages/core/useTransition/index.test.ts b/packages/core/useTransition/index.test.ts index 4394bb31f6a..5b6360f87a9 100644 --- a/packages/core/useTransition/index.test.ts +++ b/packages/core/useTransition/index.test.ts @@ -143,4 +143,38 @@ describe('useTransition', () => { expect(onStarted.mock.calls.length).toBe(1) expect(onFinished.mock.calls.length).toBe(1) }) + + it('transitions between vectors', async() => { + const vm = useSetup(() => { + const baseVector = ref([0, 0]) + + const transitionedVector = useTransition(baseVector, { + duration: 100, + }) + + return { + baseVector, + transitionedVector, + } + }) + + expect(vm.transitionedVector).toEqual([0, 0]) + + vm.baseVector = [1, 1] + + await promiseTimeout(50) + expect(vm.transitionedVector[0] > 0 && vm.transitionedVector[0] < 1).toBe(true) + expect(vm.transitionedVector[1] > 0 && vm.transitionedVector[1] < 1).toBe(true) + + await promiseTimeout(100) + expect(vm.transitionedVector).toEqual([1, 1]) + + vm.baseVector.splice(0, 1, 0) + + await promiseTimeout(50) + expect(vm.transitionedVector[0] > 0 && vm.transitionedVector[0] < 1).toBe(true) + + await promiseTimeout(100) + expect(vm.transitionedVector).toEqual([0, 1]) + }) }) diff --git a/packages/core/useTransition/index.ts b/packages/core/useTransition/index.ts index 5154b02af6b..64810c21fb7 100644 --- a/packages/core/useTransition/index.ts +++ b/packages/core/useTransition/index.ts @@ -1,5 +1,5 @@ import { useRafFn } from '../useRafFn' -import { computed, Ref, ref, unref, watch } from 'vue-demi' +import { computed, Ref, ref, unref, UnwrapRef, watch } from 'vue-demi' import { clamp, isFunction, MaybeRef, noop } from '@vueuse/shared' /** @@ -89,7 +89,7 @@ export const TransitionPresets: Record = { * @param source * @param options */ -export function useTransition(source: Ref, options: TransitionOptions = {}) { +export function useTransition>(source: T, options: TransitionOptions = {}) { const { duration = 500, onFinished = noop, @@ -97,7 +97,9 @@ export function useTransition(source: Ref, options: TransitionOptions = transition = (n: number) => n, } = options - const output = ref(source.value) + const sourceVector = computed(() => Array.isArray(source.value) ? source.value : [source.value]) + + const outputVector = ref(sourceVector.value.slice(0)) const currentTransition = computed(() => { const t = unref(transition) @@ -105,16 +107,16 @@ export function useTransition(source: Ref, options: TransitionOptions = }) let currentDuration = 0 - let diff = 0 + let diff: number[] = [] let endAt = 0 let startAt = 0 - let startValue = 0 + let startValue: number[] = [] const { resume, pause } = useRafFn(() => { const now = Date.now() const progress = clamp(1 - ((endAt - now) / currentDuration), 0, 1) - output.value = startValue + (diff * currentTransition.value(progress)) + outputVector.value = startValue.map((val, i) => val + (diff[i] * currentTransition.value(progress))) if (progress >= 1) { pause() @@ -122,18 +124,20 @@ export function useTransition(source: Ref, options: TransitionOptions = } }, { immediate: false }) - watch(source, () => { + watch(sourceVector, () => { pause() currentDuration = unref(duration) - diff = source.value - output.value - startValue = output.value + diff = outputVector.value.map((n, i) => sourceVector.value[i] - outputVector.value[i]) + startValue = outputVector.value.slice(0) startAt = Date.now() endAt = startAt + currentDuration resume() onStarted() - }) + }, { deep: true }) - return output + return Array.isArray(source.value) + ? computed(() => outputVector.value as UnwrapRef) + : computed(() => outputVector.value[0] as UnwrapRef) } diff --git a/packages/shared/utils/is.ts b/packages/shared/utils/is.ts index 15d1d75204b..a3df155c8e9 100644 --- a/packages/shared/utils/is.ts +++ b/packages/shared/utils/is.ts @@ -16,3 +16,8 @@ export const now = () => Date.now() export const timestamp = () => +Date.now() export const clamp = (n: number, min: number, max: number) => Math.min(max, Math.max(min, n)) export const noop = () => {} +export const rand = (min: number, max: number) => { + min = Math.ceil(min) + max = Math.floor(max) + return Math.floor(Math.random() * (max - min + 1)) + min +} From ca1dc9483ef18e5eaca9656116e3a3b3638b915b Mon Sep 17 00:00:00 2001 From: Scott Bedard Date: Wed, 17 Mar 2021 21:09:00 -0500 Subject: [PATCH 02/84] refactor(useTransition): cleaning up (#385) --- packages/core/useTransition/demo.vue | 2 +- packages/core/useTransition/index.md | 43 ++++-- packages/core/useTransition/index.spec.ts | 170 ++++++++++++++++++++ packages/core/useTransition/index.test.ts | 180 ---------------------- packages/core/useTransition/index.ts | 143 ++++++++++------- packages/shared/utils/index.ts | 4 + 6 files changed, 297 insertions(+), 245 deletions(-) create mode 100644 packages/core/useTransition/index.spec.ts delete mode 100644 packages/core/useTransition/index.test.ts diff --git a/packages/core/useTransition/demo.vue b/packages/core/useTransition/demo.vue index a09736eb44b..12d2675093d 100644 --- a/packages/core/useTransition/demo.vue +++ b/packages/core/useTransition/demo.vue @@ -107,7 +107,7 @@ const toggle = () => { } .vector.track .relative { - padding-bottom: 56.25%; + padding-bottom: 30%; } .vector.track .sled { diff --git a/packages/core/useTransition/index.md b/packages/core/useTransition/index.md index 683b892083d..33559ef8c7a 100644 --- a/packages/core/useTransition/index.md +++ b/packages/core/useTransition/index.md @@ -8,22 +8,49 @@ Transition between values ## Usage +For simple transitions, provide a numeric source value. When this changes, a new transition will begin. If the source changes while a transition is in progress, a new transition will begin from where the previous one was interrupted. + ```js -import { useTransition, TransitionPresets } from '@vueuse/core' +import { ref } from 'vue' +import { useTransition } from '@vueuse/core' -useTransition(source, { +const source = ref(0) + +const output = useTransition(source, { duration: 1000, - transition: TransitionPresets.easeInOutCubic, }) ``` -The following transitions are available via the `TransitionPresets` constant. +To synchronize transitions, use an array of values. To demonstrate this, here we'll transition between color values. + +```js +const source = ref([0, 0, 0]) + +const output = useTransition(source) + +const color = computed(() => { + const [r, g, b] = output.value + return `rgb(${r}, ${g}, ${b})` +}) +``` + +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). + +```js +useTransition(source, { + transition: [0.75, 0, 0.25, 1], +}) +``` + +The following common transitions are available via the `TransitionPresets` constant. - [`linear`](https://cubic-bezier.com/#0,0,1,1) - [`easeInSine`](https://cubic-bezier.com/#.12,0,.39,0) - [`easeOutSine`](https://cubic-bezier.com/#.61,1,.88,1) +- [`easeInOutSine`](https://cubic-bezier.com/#.37,0,.63,1) - [`easeInQuad`](https://cubic-bezier.com/#.11,0,.5,0) - [`easeOutQuad`](https://cubic-bezier.com/#.5,1,.89,1) +- [`easeInOutQuad`](https://cubic-bezier.com/#.45,0,.55,1) - [`easeInCubic`](https://cubic-bezier.com/#.32,0,.67,0) - [`easeOutCubic`](https://cubic-bezier.com/#.33,1,.68,1) - [`easeInOutCubic`](https://cubic-bezier.com/#.65,0,.35,1) @@ -43,14 +70,6 @@ The following transitions are available via the `TransitionPresets` constant. - [`easeOutBack`](https://cubic-bezier.com/#.34,1.56,.64,1) - [`easeInOutBack`](https://cubic-bezier.com/#.68,-.6,.32,1.6) -Custom transitions can be defined 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). - -```js -useTransition(source, { - transition: [0.75, 0, 0.25, 1], -}) -``` - For more complex transitions, a custom function can be provided. ```js diff --git a/packages/core/useTransition/index.spec.ts b/packages/core/useTransition/index.spec.ts new file mode 100644 index 00000000000..580e3afec26 --- /dev/null +++ b/packages/core/useTransition/index.spec.ts @@ -0,0 +1,170 @@ +import { promiseTimeout } from '@vueuse/shared' +import { ref } from 'vue-demi' +import { useTransition } from '.' + +const expectBetween = (val: number, floor: number, ceiling: number) => { + expect(val).toBeGreaterThan(floor) + expect(val).toBeLessThan(ceiling) +} + +describe('useTransition', () => { + it('transitions between numbers', async() => { + const source = ref(0) + const transition = useTransition(source, { duration: 100 }) + + expect(transition.value).toBe(0) + + source.value = 1 + + await promiseTimeout(50) + expectBetween(transition.value, 0, 1) + + await promiseTimeout(100) + expect(transition.value).toBe(1) + }) + + it('transitions between vectors', async() => { + const source = ref([0, 0]) + const transition = useTransition(source, { duration: 100 }) + + expect(transition.value).toEqual([0, 0]) + + source.value = [1, 1] + + await promiseTimeout(50) + expectBetween(transition.value[0], 0, 1) + expectBetween(transition.value[1], 0, 1) + + await promiseTimeout(100) + expect(transition.value[0]).toBe(1) + expect(transition.value[1]).toBe(1) + }) + + it('supports cubic bezier curves', async() => { + const source = ref(0) + + // https://cubic-bezier.com/#0,2,0,1 + const easeOutBack = useTransition(source, { + duration: 100, + transition: [0, 2, 0, 1], + }) + + // https://cubic-bezier.com/#1,0,1,-1 + const easeInBack = useTransition(source, { + duration: 100, + transition: [1, 0, 1, -1], + }) + + source.value = 1 + + await promiseTimeout(50) + expectBetween(easeOutBack.value, 1, 2) + expectBetween(easeInBack.value, -1, 0) + + await promiseTimeout(100) + expect(easeOutBack.value).toBe(1) + expect(easeInBack.value).toBe(1) + }) + + it('supports custom easing functions', async() => { + const source = ref(0) + const linear = jest.fn(n => n) + const transition = useTransition(source, { + duration: 100, + transition: linear, + }) + + expect(linear).not.toHaveBeenCalled() + + source.value = 1 + + await promiseTimeout(50) + expect(linear).toHaveBeenCalled() + expectBetween(transition.value, 0, 1) + + await promiseTimeout(100) + expect(transition.value).toBe(1) + }) + + it('supports dynamic transitions', async() => { + const source = ref(0) + const first = jest.fn(n => n) + const second = jest.fn(n => n) + const easingFn = ref(first) + + useTransition(source, { + duration: 100, + transition: easingFn, + }) + + expect(first).not.toHaveBeenCalled() + expect(second).not.toHaveBeenCalled() + + source.value = 1 + + await promiseTimeout(50) + expect(first).toHaveBeenCalled() + expect(second).not.toHaveBeenCalled() + + first.mockReset() + second.mockReset() + + easingFn.value = second + source.value = 2 + + await promiseTimeout(100) + expect(first).not.toHaveBeenCalled() + expect(second).toHaveBeenCalled() + }) + + it('supports dynamic durations', async() => { + const source = ref(0) + const duration = ref(100) + const transition = useTransition(source, { duration }) + + source.value = 1 + + await promiseTimeout(50) + expectBetween(transition.value, 0, 1) + + await promiseTimeout(100) + expect(transition.value).toBe(1) + + duration.value = 200 + source.value = 2 + + await promiseTimeout(150) + expectBetween(transition.value, 1, 2) + + await promiseTimeout(100) + expect(transition.value).toBe(2) + }) + + it('fires onStarted and onFinished callbacks', async() => { + const source = ref(0) + const onStarted = jest.fn() + const onFinished = jest.fn() + + useTransition(source, { + duration: 100, + onStarted, + onFinished, + }) + + expect(onStarted).not.toHaveBeenCalled() + expect(onFinished).not.toHaveBeenCalled() + + source.value = 1 + + await promiseTimeout(50) + expect(onStarted).toHaveBeenCalled() + expect(onFinished).not.toHaveBeenCalled() + + onStarted.mockReset() + onFinished.mockReset() + + await promiseTimeout(100) + expect(onStarted).not.toHaveBeenCalled() + expect(onFinished).toHaveBeenCalled() + }) +}) diff --git a/packages/core/useTransition/index.test.ts b/packages/core/useTransition/index.test.ts deleted file mode 100644 index 5b6360f87a9..00000000000 --- a/packages/core/useTransition/index.test.ts +++ /dev/null @@ -1,180 +0,0 @@ -import { nextTick, ref } from 'vue-demi' -import { promiseTimeout } from '@vueuse/shared' -import { useTransition, TransitionPresets } from '.' - -describe('useTransition', () => { - it('transitions between values', async() => { - const baseValue = ref(0) - - const transitionedValue = useTransition(baseValue, { - duration: 80, - transition: [0, 0, 1, 1], // a simple linear transition - }) - - // both values should start at zero - expect(baseValue.value).toBe(0) - expect(transitionedValue.value).toBe(0) - - // changing the base value should start the transition - baseValue.value = 1 - - // half way through the transition the base value should be 1, - // and the transitioned value should be approximately 0.5 - await promiseTimeout(50) - expect(baseValue.value).toBe(1) - expect(transitionedValue.value > 0 && transitionedValue.value < 1).toBe(true) - - // once the transition is complete, both values should be 1 - await promiseTimeout(100) - expect(baseValue.value).toBe(1) - expect(transitionedValue.value).toBe(1) - }) - - it('exposes named presets', async() => { - const baseValue = ref(0) - - const transitionedValue = useTransition(baseValue, { - duration: 80, - transition: TransitionPresets.linear, - }) - - baseValue.value = 1 - - await promiseTimeout(50) - expect(transitionedValue.value > 0 && transitionedValue.value < 1).toBe(true) - - await promiseTimeout(100) - expect(transitionedValue.value).toBe(1) - }) - - it('supports custom function transitions', async() => { - const baseValue = ref(0) - - const transitionedValue = useTransition(baseValue, { - duration: 80, - transition: n => n, - }) - - baseValue.value = 1 - - await promiseTimeout(50) - expect(transitionedValue.value > 0 && transitionedValue.value < 1).toBe(true) - - await promiseTimeout(100) - expect(transitionedValue.value).toBe(1) - }) - - it('supports dynamic transitions', async() => { - const linear = jest.fn(n => n) - const easeInQuad = jest.fn(n => n * n) - const baseValue = ref(0) - const transition = ref(linear) - - useTransition(baseValue, { - duration: 100, - transition, - }) - - expect(linear).not.toHaveBeenCalled() - expect(easeInQuad).not.toHaveBeenCalled() - - baseValue.value++ - await nextTick() - - expect(linear).toHaveBeenCalled() - expect(easeInQuad).not.toHaveBeenCalled() - - transition.value = easeInQuad - baseValue.value++ - await nextTick() - - expect(easeInQuad).toHaveBeenCalled() - }) - - it('support dynamic transition durations', async() => { - const baseValue = ref(0) - const duration = ref(100) - - const transitionedValue = useTransition(baseValue, { - duration, - transition: n => n, - }) - - // first transition should take 100ms - baseValue.value = 1 - - await promiseTimeout(150) - expect(transitionedValue.value).toBe(1) - - // second transition should take 200ms - duration.value = 200 - baseValue.value = 2 - - await promiseTimeout(150) - expect(transitionedValue.value < 2).toBe(true) - - await promiseTimeout(100) - expect(transitionedValue.value).toBe(2) - }) - - it('calls onStarted and onFinished callbacks', async() => { - const onStarted = jest.fn() - const onFinished = jest.fn() - - const baseValue = ref(0) - - useTransition(baseValue, { - duration: 100, - onFinished, - onStarted, - transition: n => n, - }) - - expect(onStarted).not.toHaveBeenCalled() - expect(onFinished).not.toHaveBeenCalled() - - baseValue.value = 1 - await nextTick() - - expect(onStarted).toHaveBeenCalled() - expect(onFinished).not.toHaveBeenCalled() - - await promiseTimeout(150) - expect(onStarted.mock.calls.length).toBe(1) - expect(onFinished.mock.calls.length).toBe(1) - }) - - it('transitions between vectors', async() => { - const vm = useSetup(() => { - const baseVector = ref([0, 0]) - - const transitionedVector = useTransition(baseVector, { - duration: 100, - }) - - return { - baseVector, - transitionedVector, - } - }) - - expect(vm.transitionedVector).toEqual([0, 0]) - - vm.baseVector = [1, 1] - - await promiseTimeout(50) - expect(vm.transitionedVector[0] > 0 && vm.transitionedVector[0] < 1).toBe(true) - expect(vm.transitionedVector[1] > 0 && vm.transitionedVector[1] < 1).toBe(true) - - await promiseTimeout(100) - expect(vm.transitionedVector).toEqual([1, 1]) - - vm.baseVector.splice(0, 1, 0) - - await promiseTimeout(50) - expect(vm.transitionedVector[0] > 0 && vm.transitionedVector[0] < 1).toBe(true) - - await promiseTimeout(100) - expect(vm.transitionedVector).toEqual([0, 1]) - }) -}) diff --git a/packages/core/useTransition/index.ts b/packages/core/useTransition/index.ts index 64810c21fb7..1fefabdbc7a 100644 --- a/packages/core/useTransition/index.ts +++ b/packages/core/useTransition/index.ts @@ -1,6 +1,6 @@ +import { computed, ComputedRef, ref, Ref, unref, watch } from 'vue-demi' +import { clamp, identity as linear, isFunction, isNumber, MaybeRef, noop } from '@vueuse/shared' import { useRafFn } from '../useRafFn' -import { computed, Ref, ref, unref, UnwrapRef, watch } from 'vue-demi' -import { clamp, isFunction, MaybeRef, noop } from '@vueuse/shared' /** * Cubic bezier points @@ -15,40 +15,26 @@ type EasingFunction = (n: number) => number /** * Transition options */ -interface TransitionOptions { +export type TransitionOptions = { + /** + * Transition duration in milliseconds + */ duration?: MaybeRef - onFinished?: () => unknown - onStarted?: () => unknown - transition?: MaybeRef -} - -/** - * Create an easing function from cubic bezier points. - */ -function createEasingFunction([p0, p1, p2, p3]: CubicBezierPoints): EasingFunction { - const a = (a1: number, a2: number) => 1 - 3 * a2 + 3 * a1 - const b = (a1: number, a2: number) => 3 * a2 - 6 * a1 - const c = (a1: number) => 3 * a1 - - const calcBezier = (t: number, a1: number, a2: number) => ((a(a1, a2) * t + b(a1, a2)) * t + c(a1)) * t - - const getSlope = (t: number, a1: number, a2: number) => 3 * a(a1, a2) * t * t + 2 * b(a1, a2) * t + c(a1) - - const getTforX = (x: number) => { - let aGuessT = x - for (let i = 0; i < 4; ++i) { - const currentSlope = getSlope(aGuessT, p0, p2) - if (currentSlope === 0) - return aGuessT - const currentX = calcBezier(aGuessT, p0, p2) - x - aGuessT -= currentX / currentSlope - } + /** + * Callback to execute after transition finishes + */ + onFinished?: () => void - return aGuessT - } + /** + * Callback to execute after transition starts + */ + onStarted?: () => void - return (x: number) => p0 === p1 && p2 === p3 ? x : calcBezier(getTforX(x), p1, p3) + /** + * Easing function or cubic bezier points for calculating transition values + */ + transition?: MaybeRef } /** @@ -56,12 +42,14 @@ function createEasingFunction([p0, p1, p2, p3]: CubicBezierPoints): EasingFuncti * * @see https://easings.net */ -export const TransitionPresets: Record = { - linear: [0, 0, 1, 1], +export const TransitionPresets: Record = { + linear, easeInSine: [0.12, 0, 0.39, 0], easeOutSine: [0.61, 1, 0.88, 1], + easeInOutSine: [0.37, 0, 0.63, 1], easeInQuad: [0.11, 0, 0.5, 0], easeOutQuad: [0.5, 1, 0.89, 1], + easeInOutQuad: [0.45, 0, 0.55, 1], easeInCubic: [0.32, 0, 0.67, 0], easeOutCubic: [0.33, 1, 0.68, 1], easeInOutCubic: [0.65, 0, 0.35, 1], @@ -82,6 +70,44 @@ export const TransitionPresets: Record = { easeInOutBack: [0.68, -0.6, 0.32, 1.6], } +/** + * Create an easing function from cubic bezier points. + */ +function createEasingFunction([p0, p1, p2, p3]: CubicBezierPoints): EasingFunction { + const a = (a1: number, a2: number) => 1 - 3 * a2 + 3 * a1 + const b = (a1: number, a2: number) => 3 * a2 - 6 * a1 + const c = (a1: number) => 3 * a1 + + const calcBezier = (t: number, a1: number, a2: number) => ((a(a1, a2) * t + b(a1, a2)) * t + c(a1)) * t + + const getSlope = (t: number, a1: number, a2: number) => 3 * a(a1, a2) * t * t + 2 * b(a1, a2) * t + c(a1) + + const getTforX = (x: number) => { + let aGuessT = x + + for (let i = 0; i < 4; ++i) { + const currentSlope = getSlope(aGuessT, p0, p2) + if (currentSlope === 0) + return aGuessT + const currentX = calcBezier(aGuessT, p0, p2) - x + aGuessT -= currentX / currentSlope + } + + return aGuessT + } + + return (x: number) => p0 === p1 && p2 === p3 ? x : calcBezier(getTforX(x), p1, p3) +} + +// option 1: reactive number +export function useTransition(source: Ref, options?: TransitionOptions): ComputedRef + +// option 2: static array of possibly reactive numbers +export function useTransition[]>(source: [...T], options?: TransitionOptions): ComputedRef<{ [K in keyof T]: number }> + +// option 3: reactive array of numbers +export function useTransition>(source: T, options?: TransitionOptions): ComputedRef + /** * Transition between values. * @@ -89,34 +115,48 @@ export const TransitionPresets: Record = { * @param source * @param options */ -export function useTransition>(source: T, options: TransitionOptions = {}) { +export function useTransition( + source: Ref | MaybeRef[], + options: TransitionOptions = {}, +): ComputedRef { const { - duration = 500, + duration = 1000, onFinished = noop, onStarted = noop, - transition = (n: number) => n, + transition = linear, } = options - const sourceVector = computed(() => Array.isArray(source.value) ? source.value : [source.value]) - - const outputVector = ref(sourceVector.value.slice(0)) - + // current easing function const currentTransition = computed(() => { const t = unref(transition) return isFunction(t) ? t : createEasingFunction(t) }) - let currentDuration = 0 - let diff: number[] = [] - let endAt = 0 - let startAt = 0 - let startValue: number[] = [] + // raw source value + const sourceValue = computed(() => { + const s = unref(source) + return isNumber(s) ? s : s.map(unref) as number[] + }) + + // normalized source vector + const sourceVector = computed(() => isNumber(sourceValue.value) ? [sourceValue.value] : sourceValue.value) + + // transitioned output vector + const outputVector = ref(sourceVector.value.slice(0)) + + // current transition values + let currentDuration: number + let diffVector: number[] + let endAt: number + let startAt: number + let startVector: number[] + // transition loop const { resume, pause } = useRafFn(() => { const now = Date.now() const progress = clamp(1 - ((endAt - now) / currentDuration), 0, 1) - outputVector.value = startValue.map((val, i) => val + (diff[i] * currentTransition.value(progress))) + outputVector.value = startVector.map((val, i) => val + ((diffVector[i] ?? 0) * currentTransition.value(progress))) if (progress >= 1) { pause() @@ -124,12 +164,13 @@ export function useTransition>(source: T, optio } }, { immediate: false }) + // start the animation loop when source vector changes watch(sourceVector, () => { pause() currentDuration = unref(duration) - diff = outputVector.value.map((n, i) => sourceVector.value[i] - outputVector.value[i]) - startValue = outputVector.value.slice(0) + diffVector = outputVector.value.map((n, i) => (sourceVector.value[i] ?? 0) - (outputVector.value[i] ?? 0)) + startVector = outputVector.value.slice(0) startAt = Date.now() endAt = startAt + currentDuration @@ -137,7 +178,5 @@ export function useTransition>(source: T, optio onStarted() }, { deep: true }) - return Array.isArray(source.value) - ? computed(() => outputVector.value as UnwrapRef) - : computed(() => outputVector.value[0] as UnwrapRef) + return computed(() => isNumber(sourceValue.value) ? outputVector.value[0] : outputVector.value) } diff --git a/packages/shared/utils/index.ts b/packages/shared/utils/index.ts index 547af85b21f..cf890ec9718 100644 --- a/packages/shared/utils/index.ts +++ b/packages/shared/utils/index.ts @@ -15,6 +15,10 @@ export function promiseTimeout( }) } +export function identity(arg: T): T { + return arg +} + export interface SingletonPromiseReturn { (): Promise /** From 4099e0fbbee203293a40b5729f32d33ad836d97e Mon Sep 17 00:00:00 2001 From: Fabian Date: Thu, 25 Mar 2021 07:07:37 +0100 Subject: [PATCH 03/84] refactor(useWebWorkerFn): Small doc and type improvements (#382) Co-authored-by: Anthony Fu --- packages/core/useWebWorkerFn/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/useWebWorkerFn/index.md b/packages/core/useWebWorkerFn/index.md index ce9636604cf..ece083afb1e 100644 --- a/packages/core/useWebWorkerFn/index.md +++ b/packages/core/useWebWorkerFn/index.md @@ -75,7 +75,7 @@ export interface WebWorkerOptions extends ConfigurableWindow { */ export declare const useWebWorkerFn: any>( fn: T, - options?: WebWorkerOptions + { dependencies = [], timeout, window = defaultWindow }: Partial = {}, ) => { workerFn: (...fnArgs: Parameters) => Promise> workerStatus: Ref From 920abdd658d93762a09cfd785298fac954d71220 Mon Sep 17 00:00:00 2001 From: Anthony Fu Date: Thu, 25 Mar 2021 14:38:51 +0800 Subject: [PATCH 04/84] feat: pwa reload prompt --- yarn.lock | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/yarn.lock b/yarn.lock index 21fe6297bce..7451f3b4650 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2031,6 +2031,14 @@ "@typescript-eslint/types" "4.22.1" eslint-visitor-keys "^2.0.0" +"@typescript-eslint/visitor-keys@4.19.0": + version "4.19.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.19.0.tgz#cbea35109cbd9b26e597644556be4546465d8f7f" + integrity sha512-aGPS6kz//j7XLSlgpzU2SeTqHPsmRYxFztj2vPuMMFJXZudpRSehE3WCV+BaxwZFvfAqMoSd86TEuM0PQ59E/A== + dependencies: + "@typescript-eslint/types" "4.19.0" + eslint-visitor-keys "^2.0.0" + "@vitejs/plugin-vue@^1.1.4": version "1.2.2" resolved "https://registry.yarnpkg.com/@vitejs/plugin-vue/-/plugin-vue-1.2.2.tgz#b0038fc11b9099f4cd01fcbf0ee419adda417b52" @@ -7110,6 +7118,13 @@ rollup@^2.33.2, rollup@^2.38.5, rollup@^2.43.1, rollup@^2.45.2, rollup@^2.47.0: optionalDependencies: fsevents "~2.3.1" +rollup@^2.42.4: + version "2.42.4" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.42.4.tgz#97c910a48bd0db6aaa4271dd48745870cbbbf970" + integrity sha512-Zqv3EvNfcllBHyyEUM754npqsZw82VIjK34cDQMwrQ1d6aqxzeYu5yFb7smGkPU4C1Bj7HupIMeT6WU7uIdnMw== + optionalDependencies: + fsevents "~2.3.1" + rsvp@^4.8.4: version "4.8.5" resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-4.8.5.tgz#c8f155311d167f68f21e168df71ec5b083113734" @@ -7225,6 +7240,13 @@ semver@^6.0.0, semver@^6.1.0, semver@^6.1.1, semver@^6.1.2, semver@^6.3.0: resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== +semver@^7.3.4: + version "7.3.5" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.5.tgz#0b621c879348d8998e4b0e4be94b3f12e6018ef7" + integrity sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ== + dependencies: + lru-cache "^6.0.0" + serialize-javascript@^2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-2.1.2.tgz#ecec53b0e0317bdc95ef76ab7074b7384785fa61" From 8a5f23342f73ee67f1b6f5ed9df0a62ee066b159 Mon Sep 17 00:00:00 2001 From: Anthony Fu Date: Thu, 25 Mar 2021 14:39:45 +0800 Subject: [PATCH 05/84] chore: update docs --- packages/core/useWebWorkerFn/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/useWebWorkerFn/index.md b/packages/core/useWebWorkerFn/index.md index ece083afb1e..ce9636604cf 100644 --- a/packages/core/useWebWorkerFn/index.md +++ b/packages/core/useWebWorkerFn/index.md @@ -75,7 +75,7 @@ export interface WebWorkerOptions extends ConfigurableWindow { */ export declare const useWebWorkerFn: any>( fn: T, - { dependencies = [], timeout, window = defaultWindow }: Partial = {}, + options?: WebWorkerOptions ) => { workerFn: (...fnArgs: Parameters) => Promise> workerStatus: Ref From d99247d6656615a22e284c724185501fb3ab62c7 Mon Sep 17 00:00:00 2001 From: Fabian Date: Thu, 25 Mar 2021 07:07:37 +0100 Subject: [PATCH 06/84] refactor(useWebWorkerFn): Small doc and type improvements (#382) Co-authored-by: Anthony Fu --- packages/core/useWebWorkerFn/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/useWebWorkerFn/index.md b/packages/core/useWebWorkerFn/index.md index ce9636604cf..ece083afb1e 100644 --- a/packages/core/useWebWorkerFn/index.md +++ b/packages/core/useWebWorkerFn/index.md @@ -75,7 +75,7 @@ export interface WebWorkerOptions extends ConfigurableWindow { */ export declare const useWebWorkerFn: any>( fn: T, - options?: WebWorkerOptions + { dependencies = [], timeout, window = defaultWindow }: Partial = {}, ) => { workerFn: (...fnArgs: Parameters) => Promise> workerStatus: Ref From 74b5323987d04471351a798a4533da7abf8d9c7e Mon Sep 17 00:00:00 2001 From: Anthony Fu Date: Thu, 25 Mar 2021 14:39:45 +0800 Subject: [PATCH 07/84] chore: update docs --- packages/core/useWebWorkerFn/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/useWebWorkerFn/index.md b/packages/core/useWebWorkerFn/index.md index ece083afb1e..ce9636604cf 100644 --- a/packages/core/useWebWorkerFn/index.md +++ b/packages/core/useWebWorkerFn/index.md @@ -75,7 +75,7 @@ export interface WebWorkerOptions extends ConfigurableWindow { */ export declare const useWebWorkerFn: any>( fn: T, - { dependencies = [], timeout, window = defaultWindow }: Partial = {}, + options?: WebWorkerOptions ) => { workerFn: (...fnArgs: Parameters) => Promise> workerStatus: Ref From 83b8f0bca8fbe8332b70bea4ab5d991c5a2bf407 Mon Sep 17 00:00:00 2001 From: Anthony Fu Date: Thu, 25 Mar 2021 15:22:18 +0800 Subject: [PATCH 08/84] test: simpilfy tests for useTransition --- packages/core/useTransition/index.test.ts | 146 ++++++++++++++++++++++ 1 file changed, 146 insertions(+) create mode 100644 packages/core/useTransition/index.test.ts diff --git a/packages/core/useTransition/index.test.ts b/packages/core/useTransition/index.test.ts new file mode 100644 index 00000000000..4394bb31f6a --- /dev/null +++ b/packages/core/useTransition/index.test.ts @@ -0,0 +1,146 @@ +import { nextTick, ref } from 'vue-demi' +import { promiseTimeout } from '@vueuse/shared' +import { useTransition, TransitionPresets } from '.' + +describe('useTransition', () => { + it('transitions between values', async() => { + const baseValue = ref(0) + + const transitionedValue = useTransition(baseValue, { + duration: 80, + transition: [0, 0, 1, 1], // a simple linear transition + }) + + // both values should start at zero + expect(baseValue.value).toBe(0) + expect(transitionedValue.value).toBe(0) + + // changing the base value should start the transition + baseValue.value = 1 + + // half way through the transition the base value should be 1, + // and the transitioned value should be approximately 0.5 + await promiseTimeout(50) + expect(baseValue.value).toBe(1) + expect(transitionedValue.value > 0 && transitionedValue.value < 1).toBe(true) + + // once the transition is complete, both values should be 1 + await promiseTimeout(100) + expect(baseValue.value).toBe(1) + expect(transitionedValue.value).toBe(1) + }) + + it('exposes named presets', async() => { + const baseValue = ref(0) + + const transitionedValue = useTransition(baseValue, { + duration: 80, + transition: TransitionPresets.linear, + }) + + baseValue.value = 1 + + await promiseTimeout(50) + expect(transitionedValue.value > 0 && transitionedValue.value < 1).toBe(true) + + await promiseTimeout(100) + expect(transitionedValue.value).toBe(1) + }) + + it('supports custom function transitions', async() => { + const baseValue = ref(0) + + const transitionedValue = useTransition(baseValue, { + duration: 80, + transition: n => n, + }) + + baseValue.value = 1 + + await promiseTimeout(50) + expect(transitionedValue.value > 0 && transitionedValue.value < 1).toBe(true) + + await promiseTimeout(100) + expect(transitionedValue.value).toBe(1) + }) + + it('supports dynamic transitions', async() => { + const linear = jest.fn(n => n) + const easeInQuad = jest.fn(n => n * n) + const baseValue = ref(0) + const transition = ref(linear) + + useTransition(baseValue, { + duration: 100, + transition, + }) + + expect(linear).not.toHaveBeenCalled() + expect(easeInQuad).not.toHaveBeenCalled() + + baseValue.value++ + await nextTick() + + expect(linear).toHaveBeenCalled() + expect(easeInQuad).not.toHaveBeenCalled() + + transition.value = easeInQuad + baseValue.value++ + await nextTick() + + expect(easeInQuad).toHaveBeenCalled() + }) + + it('support dynamic transition durations', async() => { + const baseValue = ref(0) + const duration = ref(100) + + const transitionedValue = useTransition(baseValue, { + duration, + transition: n => n, + }) + + // first transition should take 100ms + baseValue.value = 1 + + await promiseTimeout(150) + expect(transitionedValue.value).toBe(1) + + // second transition should take 200ms + duration.value = 200 + baseValue.value = 2 + + await promiseTimeout(150) + expect(transitionedValue.value < 2).toBe(true) + + await promiseTimeout(100) + expect(transitionedValue.value).toBe(2) + }) + + it('calls onStarted and onFinished callbacks', async() => { + const onStarted = jest.fn() + const onFinished = jest.fn() + + const baseValue = ref(0) + + useTransition(baseValue, { + duration: 100, + onFinished, + onStarted, + transition: n => n, + }) + + expect(onStarted).not.toHaveBeenCalled() + expect(onFinished).not.toHaveBeenCalled() + + baseValue.value = 1 + await nextTick() + + expect(onStarted).toHaveBeenCalled() + expect(onFinished).not.toHaveBeenCalled() + + await promiseTimeout(150) + expect(onStarted.mock.calls.length).toBe(1) + expect(onFinished.mock.calls.length).toBe(1) + }) +}) From 734c38844e7a7cd16faa0bf8cf92d59dd5945e3b Mon Sep 17 00:00:00 2001 From: Anthony Fu Date: Thu, 25 Mar 2021 15:32:11 +0800 Subject: [PATCH 09/84] chore: fix tests --- yarn.lock | 7 ------- 1 file changed, 7 deletions(-) diff --git a/yarn.lock b/yarn.lock index 7451f3b4650..b710c9322d5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7240,13 +7240,6 @@ semver@^6.0.0, semver@^6.1.0, semver@^6.1.1, semver@^6.1.2, semver@^6.3.0: resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== -semver@^7.3.4: - version "7.3.5" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.5.tgz#0b621c879348d8998e4b0e4be94b3f12e6018ef7" - integrity sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ== - dependencies: - lru-cache "^6.0.0" - serialize-javascript@^2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-2.1.2.tgz#ecec53b0e0317bdc95ef76ab7074b7384785fa61" From b5d5dec6095d0a101dd47ac9e5d9478430f63240 Mon Sep 17 00:00:00 2001 From: Scott Bedard Date: Fri, 26 Mar 2021 19:49:27 -0500 Subject: [PATCH 10/84] feat(useTransition): support for delayed transitions (#386) --- packages/core/useTransition/index.md | 15 +++++---- packages/core/useTransition/index.spec.ts | 39 ++++++++++++++++++++++ packages/core/useTransition/index.ts | 18 ++++++++-- packages/shared/useTimeoutFn/index.spec.ts | 24 +++++++++++++ packages/shared/useTimeoutFn/index.ts | 8 ++--- 5 files changed, 91 insertions(+), 13 deletions(-) create mode 100644 packages/shared/useTimeoutFn/index.spec.ts diff --git a/packages/core/useTransition/index.md b/packages/core/useTransition/index.md index 33559ef8c7a..604bd5a8112 100644 --- a/packages/core/useTransition/index.md +++ b/packages/core/useTransition/index.md @@ -8,20 +8,21 @@ Transition between values ## Usage -For simple transitions, provide a numeric source value. When this changes, a new transition will begin. If the source changes while a transition is in progress, a new transition will begin from where the previous one was interrupted. +For simple transitions, provide a numeric source to watch. 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. ```js import { ref } from 'vue' -import { useTransition } from '@vueuse/core' +import { useTransition, TransitionPresets } from '@vueuse/core' const source = ref(0) const output = useTransition(source, { duration: 1000, + transition: TransitionPresets.easeInOutCubic, }) ``` -To synchronize transitions, use an array of values. To demonstrate this, here we'll transition between color values. +To synchronize transitions, use an array of numbers. As an example, here is how we could transition between colors. ```js const source = ref([0, 0, 0]) @@ -42,7 +43,7 @@ useTransition(source, { }) ``` -The following common transitions are available via the `TransitionPresets` constant. +The following transitions are available via the `TransitionPresets` constant. - [`linear`](https://cubic-bezier.com/#0,0,1,1) - [`easeInSine`](https://cubic-bezier.com/#.12,0,.39,0) @@ -66,7 +67,7 @@ The following common transitions are available via the `TransitionPresets` const - [`easeInCirc`](https://cubic-bezier.com/#.55,0,1,.45) - [`easeOutCirc`](https://cubic-bezier.com/#0,.55,.45,1) - [`easeInOutCirc`](https://cubic-bezier.com/#.85,0,.15,1) -- [`easeInBack`](https://cubic-bezier.com/#0.12,0,0.39,0) +- [`easeInBack`](https://cubic-bezier.com/#.36,0,.66,-.56) - [`easeOutBack`](https://cubic-bezier.com/#.34,1.56,.64,1) - [`easeInOutBack`](https://cubic-bezier.com/#.68,-.6,.32,1.6) @@ -86,10 +87,11 @@ useTransition(source, { }) ``` -To choreograph behavior around a transition, define `onStarted` or `onFinished` callbacks. +To control when a transition starts, set a `delay` value. To choreograph behavior around a transition, define `onStarted` or `onFinished` callbacks. ```js useTransition(source, { + delay: 1000, onStarted() { // called after the transition starts }, @@ -99,7 +101,6 @@ useTransition(source, { }) ``` - ## Type Declarations diff --git a/packages/core/useTransition/index.spec.ts b/packages/core/useTransition/index.spec.ts index 580e3afec26..a1ec3d6cb8c 100644 --- a/packages/core/useTransition/index.spec.ts +++ b/packages/core/useTransition/index.spec.ts @@ -86,6 +86,23 @@ describe('useTransition', () => { expect(transition.value).toBe(1) }) + it('supports delayed transitions', async() => { + const source = ref(0) + + const transition = useTransition(source, { + delay: 100, + duration: 100, + }) + + source.value = 1 + + await promiseTimeout(50) + expect(transition.value).toBe(0) + + await promiseTimeout(100) + expectBetween(transition.value, 0, 1) + }) + it('supports dynamic transitions', async() => { const source = ref(0) const first = jest.fn(n => n) @@ -167,4 +184,26 @@ describe('useTransition', () => { expect(onStarted).not.toHaveBeenCalled() expect(onFinished).toHaveBeenCalled() }) + + it('clears pending transitions before starting a new one', async() => { + const source = ref(0) + const onStarted = jest.fn() + const onFinished = jest.fn() + + useTransition(source, { + delay: 100, + duration: 100, + onFinished, + onStarted, + }) + + await promiseTimeout(150) + expect(onStarted).not.toHaveBeenCalled() + source.value = 1 + await promiseTimeout(50) + source.value = 2 + await promiseTimeout(250) + expect(onStarted).toHaveBeenCalledTimes(1) + expect(onFinished).toHaveBeenCalledTimes(1) + }) }) diff --git a/packages/core/useTransition/index.ts b/packages/core/useTransition/index.ts index 1fefabdbc7a..06ec08fca37 100644 --- a/packages/core/useTransition/index.ts +++ b/packages/core/useTransition/index.ts @@ -1,6 +1,7 @@ import { computed, ComputedRef, ref, Ref, unref, watch } from 'vue-demi' import { clamp, identity as linear, isFunction, isNumber, MaybeRef, noop } from '@vueuse/shared' import { useRafFn } from '../useRafFn' +import { useTimeoutFn } from '@vueuse/core' /** * Cubic bezier points @@ -16,6 +17,11 @@ type EasingFunction = (n: number) => number * Transition options */ export type TransitionOptions = { + /** + * Milliseconds to wait before starting transition + */ + delay?: MaybeRef + /** * Transition duration in milliseconds */ @@ -118,8 +124,9 @@ export function useTransition>(source: T, options?: Tran export function useTransition( source: Ref | MaybeRef[], options: TransitionOptions = {}, -): ComputedRef { +): ComputedRef { const { + delay = 0, duration = 1000, onFinished = noop, onStarted = noop, @@ -165,7 +172,7 @@ export function useTransition( }, { immediate: false }) // start the animation loop when source vector changes - watch(sourceVector, () => { + const start = () => { pause() currentDuration = unref(duration) @@ -176,6 +183,13 @@ export function useTransition( resume() onStarted() + } + + const timeout = useTimeoutFn(start, delay, false) + + watch(sourceVector, () => { + if (unref(delay) <= 0) start() + else timeout.start() }, { deep: true }) return computed(() => isNumber(sourceValue.value) ? outputVector.value[0] : outputVector.value) diff --git a/packages/shared/useTimeoutFn/index.spec.ts b/packages/shared/useTimeoutFn/index.spec.ts new file mode 100644 index 00000000000..5bb59a082ae --- /dev/null +++ b/packages/shared/useTimeoutFn/index.spec.ts @@ -0,0 +1,24 @@ +import { promiseTimeout } from '@vueuse/core' +import { ref } from 'vue-demi' +import { useTimeoutFn } from '.' + +describe('useTimeoutFn', () => { + it('supports reactive intervals', async() => { + const callback = jest.fn() + const interval = ref(0) + const { start } = useTimeoutFn(callback, interval) + + start() + await promiseTimeout(1) + expect(callback).toHaveBeenCalled() + + callback.mockReset() + interval.value = 50 + + start() + await promiseTimeout(1) + expect(callback).not.toHaveBeenCalled() + await promiseTimeout(100) + expect(callback).toHaveBeenCalled() + }) +}) diff --git a/packages/shared/useTimeoutFn/index.ts b/packages/shared/useTimeoutFn/index.ts index 11e38957ae0..256f17150e8 100644 --- a/packages/shared/useTimeoutFn/index.ts +++ b/packages/shared/useTimeoutFn/index.ts @@ -1,5 +1,5 @@ -import { Fn } from '@vueuse/shared' -import { Ref, ref } from 'vue-demi' +import { Fn, MaybeRef } from '@vueuse/shared' +import { Ref, ref, unref } from 'vue-demi' import { tryOnUnmounted } from '../tryOnUnmounted' import { isClient } from '../utils' @@ -22,7 +22,7 @@ export interface TimeoutFnResult { */ export function useTimeoutFn( cb: (...args: unknown[]) => any, - interval?: number, + interval?: MaybeRef, immediate = true, ): TimeoutFnResult { const isPending = ref(false) @@ -49,7 +49,7 @@ export function useTimeoutFn( timer = null // eslint-disable-next-line node/no-callback-literal cb(...args) - }, interval) as unknown as number + }, unref(interval)) as unknown as number } if (immediate) { From fa5dc4b34447e288feb8c475a77f004c224c0afc Mon Sep 17 00:00:00 2001 From: Scott Bedard Date: Thu, 8 Apr 2021 01:33:29 -0500 Subject: [PATCH 11/84] feat(useTransition): support for disabled transitions (#436) --- packages/core/useTransition/index.md | 4 +- packages/core/useTransition/index.spec.ts | 209 ---------------- packages/core/useTransition/index.test.ts | 236 ++++++++++++------ packages/core/useTransition/index.ts | 20 +- .../{index.spec.ts => index.test.ts} | 0 5 files changed, 180 insertions(+), 289 deletions(-) delete mode 100644 packages/core/useTransition/index.spec.ts rename packages/shared/useTimeoutFn/{index.spec.ts => index.test.ts} (100%) diff --git a/packages/core/useTransition/index.md b/packages/core/useTransition/index.md index 604bd5a8112..6fef9119305 100644 --- a/packages/core/useTransition/index.md +++ b/packages/core/useTransition/index.md @@ -8,7 +8,7 @@ Transition between values ## Usage -For simple transitions, provide a numeric source to watch. 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. +For simple transitions, provide a numeric source value to watch. 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. ```js import { ref } from 'vue' @@ -101,6 +101,8 @@ useTransition(source, { }) ``` +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. + ## Type Declarations diff --git a/packages/core/useTransition/index.spec.ts b/packages/core/useTransition/index.spec.ts deleted file mode 100644 index a1ec3d6cb8c..00000000000 --- a/packages/core/useTransition/index.spec.ts +++ /dev/null @@ -1,209 +0,0 @@ -import { promiseTimeout } from '@vueuse/shared' -import { ref } from 'vue-demi' -import { useTransition } from '.' - -const expectBetween = (val: number, floor: number, ceiling: number) => { - expect(val).toBeGreaterThan(floor) - expect(val).toBeLessThan(ceiling) -} - -describe('useTransition', () => { - it('transitions between numbers', async() => { - const source = ref(0) - const transition = useTransition(source, { duration: 100 }) - - expect(transition.value).toBe(0) - - source.value = 1 - - await promiseTimeout(50) - expectBetween(transition.value, 0, 1) - - await promiseTimeout(100) - expect(transition.value).toBe(1) - }) - - it('transitions between vectors', async() => { - const source = ref([0, 0]) - const transition = useTransition(source, { duration: 100 }) - - expect(transition.value).toEqual([0, 0]) - - source.value = [1, 1] - - await promiseTimeout(50) - expectBetween(transition.value[0], 0, 1) - expectBetween(transition.value[1], 0, 1) - - await promiseTimeout(100) - expect(transition.value[0]).toBe(1) - expect(transition.value[1]).toBe(1) - }) - - it('supports cubic bezier curves', async() => { - const source = ref(0) - - // https://cubic-bezier.com/#0,2,0,1 - const easeOutBack = useTransition(source, { - duration: 100, - transition: [0, 2, 0, 1], - }) - - // https://cubic-bezier.com/#1,0,1,-1 - const easeInBack = useTransition(source, { - duration: 100, - transition: [1, 0, 1, -1], - }) - - source.value = 1 - - await promiseTimeout(50) - expectBetween(easeOutBack.value, 1, 2) - expectBetween(easeInBack.value, -1, 0) - - await promiseTimeout(100) - expect(easeOutBack.value).toBe(1) - expect(easeInBack.value).toBe(1) - }) - - it('supports custom easing functions', async() => { - const source = ref(0) - const linear = jest.fn(n => n) - const transition = useTransition(source, { - duration: 100, - transition: linear, - }) - - expect(linear).not.toHaveBeenCalled() - - source.value = 1 - - await promiseTimeout(50) - expect(linear).toHaveBeenCalled() - expectBetween(transition.value, 0, 1) - - await promiseTimeout(100) - expect(transition.value).toBe(1) - }) - - it('supports delayed transitions', async() => { - const source = ref(0) - - const transition = useTransition(source, { - delay: 100, - duration: 100, - }) - - source.value = 1 - - await promiseTimeout(50) - expect(transition.value).toBe(0) - - await promiseTimeout(100) - expectBetween(transition.value, 0, 1) - }) - - it('supports dynamic transitions', async() => { - const source = ref(0) - const first = jest.fn(n => n) - const second = jest.fn(n => n) - const easingFn = ref(first) - - useTransition(source, { - duration: 100, - transition: easingFn, - }) - - expect(first).not.toHaveBeenCalled() - expect(second).not.toHaveBeenCalled() - - source.value = 1 - - await promiseTimeout(50) - expect(first).toHaveBeenCalled() - expect(second).not.toHaveBeenCalled() - - first.mockReset() - second.mockReset() - - easingFn.value = second - source.value = 2 - - await promiseTimeout(100) - expect(first).not.toHaveBeenCalled() - expect(second).toHaveBeenCalled() - }) - - it('supports dynamic durations', async() => { - const source = ref(0) - const duration = ref(100) - const transition = useTransition(source, { duration }) - - source.value = 1 - - await promiseTimeout(50) - expectBetween(transition.value, 0, 1) - - await promiseTimeout(100) - expect(transition.value).toBe(1) - - duration.value = 200 - source.value = 2 - - await promiseTimeout(150) - expectBetween(transition.value, 1, 2) - - await promiseTimeout(100) - expect(transition.value).toBe(2) - }) - - it('fires onStarted and onFinished callbacks', async() => { - const source = ref(0) - const onStarted = jest.fn() - const onFinished = jest.fn() - - useTransition(source, { - duration: 100, - onStarted, - onFinished, - }) - - expect(onStarted).not.toHaveBeenCalled() - expect(onFinished).not.toHaveBeenCalled() - - source.value = 1 - - await promiseTimeout(50) - expect(onStarted).toHaveBeenCalled() - expect(onFinished).not.toHaveBeenCalled() - - onStarted.mockReset() - onFinished.mockReset() - - await promiseTimeout(100) - expect(onStarted).not.toHaveBeenCalled() - expect(onFinished).toHaveBeenCalled() - }) - - it('clears pending transitions before starting a new one', async() => { - const source = ref(0) - const onStarted = jest.fn() - const onFinished = jest.fn() - - useTransition(source, { - delay: 100, - duration: 100, - onFinished, - onStarted, - }) - - await promiseTimeout(150) - expect(onStarted).not.toHaveBeenCalled() - source.value = 1 - await promiseTimeout(50) - source.value = 2 - await promiseTimeout(250) - expect(onStarted).toHaveBeenCalledTimes(1) - expect(onFinished).toHaveBeenCalledTimes(1) - }) -}) diff --git a/packages/core/useTransition/index.test.ts b/packages/core/useTransition/index.test.ts index 4394bb31f6a..8e7e08cb242 100644 --- a/packages/core/useTransition/index.test.ts +++ b/packages/core/useTransition/index.test.ts @@ -1,146 +1,230 @@ -import { nextTick, ref } from 'vue-demi' import { promiseTimeout } from '@vueuse/shared' -import { useTransition, TransitionPresets } from '.' +import { ref } from 'vue-demi' +import { useTransition } from '.' + +const expectBetween = (val: number, floor: number, ceiling: number) => { + expect(val).toBeGreaterThan(floor) + expect(val).toBeLessThan(ceiling) +} describe('useTransition', () => { - it('transitions between values', async() => { - const baseValue = ref(0) + it('transitions between numbers', async() => { + const source = ref(0) + const transition = useTransition(source, { duration: 100 }) - const transitionedValue = useTransition(baseValue, { - duration: 80, - transition: [0, 0, 1, 1], // a simple linear transition - }) + expect(transition.value).toBe(0) + + source.value = 1 + + await promiseTimeout(50) + expectBetween(transition.value, 0, 1) + + await promiseTimeout(100) + expect(transition.value).toBe(1) + }) + + it('transitions between vectors', async() => { + const source = ref([0, 0]) + const transition = useTransition(source, { duration: 100 }) - // both values should start at zero - expect(baseValue.value).toBe(0) - expect(transitionedValue.value).toBe(0) + expect(transition.value).toEqual([0, 0]) - // changing the base value should start the transition - baseValue.value = 1 + source.value = [1, 1] - // half way through the transition the base value should be 1, - // and the transitioned value should be approximately 0.5 await promiseTimeout(50) - expect(baseValue.value).toBe(1) - expect(transitionedValue.value > 0 && transitionedValue.value < 1).toBe(true) + expectBetween(transition.value[0], 0, 1) + expectBetween(transition.value[1], 0, 1) - // once the transition is complete, both values should be 1 await promiseTimeout(100) - expect(baseValue.value).toBe(1) - expect(transitionedValue.value).toBe(1) + expect(transition.value[0]).toBe(1) + expect(transition.value[1]).toBe(1) }) - it('exposes named presets', async() => { - const baseValue = ref(0) + it('supports cubic bezier curves', async() => { + const source = ref(0) + + // https://cubic-bezier.com/#0,2,0,1 + const easeOutBack = useTransition(source, { + duration: 100, + transition: [0, 2, 0, 1], + }) + + // https://cubic-bezier.com/#1,0,1,-1 + const easeInBack = useTransition(source, { + duration: 100, + transition: [1, 0, 1, -1], + }) + + source.value = 1 + + await promiseTimeout(50) + expectBetween(easeOutBack.value, 1, 2) + expectBetween(easeInBack.value, -1, 0) - const transitionedValue = useTransition(baseValue, { - duration: 80, - transition: TransitionPresets.linear, + await promiseTimeout(100) + expect(easeOutBack.value).toBe(1) + expect(easeInBack.value).toBe(1) + }) + + it('supports custom easing functions', async() => { + const source = ref(0) + const linear = jest.fn(n => n) + const transition = useTransition(source, { + duration: 100, + transition: linear, }) - baseValue.value = 1 + expect(linear).not.toHaveBeenCalled() + + source.value = 1 await promiseTimeout(50) - expect(transitionedValue.value > 0 && transitionedValue.value < 1).toBe(true) + expect(linear).toHaveBeenCalled() + expectBetween(transition.value, 0, 1) await promiseTimeout(100) - expect(transitionedValue.value).toBe(1) + expect(transition.value).toBe(1) }) - it('supports custom function transitions', async() => { - const baseValue = ref(0) + it('supports delayed transitions', async() => { + const source = ref(0) - const transitionedValue = useTransition(baseValue, { - duration: 80, - transition: n => n, + const transition = useTransition(source, { + delay: 100, + duration: 100, }) - baseValue.value = 1 + source.value = 1 await promiseTimeout(50) - expect(transitionedValue.value > 0 && transitionedValue.value < 1).toBe(true) + expect(transition.value).toBe(0) await promiseTimeout(100) - expect(transitionedValue.value).toBe(1) + expectBetween(transition.value, 0, 1) }) it('supports dynamic transitions', async() => { - const linear = jest.fn(n => n) - const easeInQuad = jest.fn(n => n * n) - const baseValue = ref(0) - const transition = ref(linear) + const source = ref(0) + const first = jest.fn(n => n) + const second = jest.fn(n => n) + const easingFn = ref(first) - useTransition(baseValue, { + useTransition(source, { duration: 100, - transition, + transition: easingFn, }) - expect(linear).not.toHaveBeenCalled() - expect(easeInQuad).not.toHaveBeenCalled() + expect(first).not.toHaveBeenCalled() + expect(second).not.toHaveBeenCalled() - baseValue.value++ - await nextTick() + source.value = 1 - expect(linear).toHaveBeenCalled() - expect(easeInQuad).not.toHaveBeenCalled() + await promiseTimeout(50) + expect(first).toHaveBeenCalled() + expect(second).not.toHaveBeenCalled() + + first.mockReset() + second.mockReset() - transition.value = easeInQuad - baseValue.value++ - await nextTick() + easingFn.value = second + source.value = 2 - expect(easeInQuad).toHaveBeenCalled() + await promiseTimeout(100) + expect(first).not.toHaveBeenCalled() + expect(second).toHaveBeenCalled() }) - it('support dynamic transition durations', async() => { - const baseValue = ref(0) + it('supports dynamic durations', async() => { + const source = ref(0) const duration = ref(100) + const transition = useTransition(source, { duration }) - const transitionedValue = useTransition(baseValue, { - duration, - transition: n => n, - }) + source.value = 1 - // first transition should take 100ms - baseValue.value = 1 + await promiseTimeout(50) + expectBetween(transition.value, 0, 1) - await promiseTimeout(150) - expect(transitionedValue.value).toBe(1) + await promiseTimeout(100) + expect(transition.value).toBe(1) - // second transition should take 200ms duration.value = 200 - baseValue.value = 2 + source.value = 2 await promiseTimeout(150) - expect(transitionedValue.value < 2).toBe(true) + expectBetween(transition.value, 1, 2) await promiseTimeout(100) - expect(transitionedValue.value).toBe(2) + expect(transition.value).toBe(2) }) - it('calls onStarted and onFinished callbacks', async() => { + it('fires onStarted and onFinished callbacks', async() => { + const source = ref(0) const onStarted = jest.fn() const onFinished = jest.fn() - const baseValue = ref(0) - - useTransition(baseValue, { + useTransition(source, { duration: 100, - onFinished, onStarted, - transition: n => n, + onFinished, }) expect(onStarted).not.toHaveBeenCalled() expect(onFinished).not.toHaveBeenCalled() - baseValue.value = 1 - await nextTick() + source.value = 1 + await promiseTimeout(50) expect(onStarted).toHaveBeenCalled() expect(onFinished).not.toHaveBeenCalled() + onStarted.mockReset() + onFinished.mockReset() + + await promiseTimeout(100) + expect(onStarted).not.toHaveBeenCalled() + expect(onFinished).toHaveBeenCalled() + }) + + it('clears pending transitions before starting a new one', async() => { + const source = ref(0) + const onStarted = jest.fn() + const onFinished = jest.fn() + + useTransition(source, { + delay: 100, + duration: 100, + onFinished, + onStarted, + }) + + await promiseTimeout(150) + expect(onStarted).not.toHaveBeenCalled() + source.value = 1 + await promiseTimeout(50) + source.value = 2 + await promiseTimeout(250) + expect(onStarted).toHaveBeenCalledTimes(1) + expect(onFinished).toHaveBeenCalledTimes(1) + }) + + it('can be disabled for sychronous changes', async() => { + const onStarted = jest.fn() + const disabled = ref(false) + const source = ref(0) + + const transition = useTransition(source, { + disabled, + duration: 100, + onStarted, + }) + + disabled.value = true + source.value = 1 + + expect(transition.value).toBe(1) await promiseTimeout(150) - expect(onStarted.mock.calls.length).toBe(1) - expect(onFinished.mock.calls.length).toBe(1) + expect(onStarted).not.toHaveBeenCalled() + disabled.value = false + expect(transition.value).toBe(1) }) }) diff --git a/packages/core/useTransition/index.ts b/packages/core/useTransition/index.ts index 06ec08fca37..1210d1d658f 100644 --- a/packages/core/useTransition/index.ts +++ b/packages/core/useTransition/index.ts @@ -22,6 +22,11 @@ export type TransitionOptions = { */ delay?: MaybeRef + /** + * Disables the transition + */ + disabled?: MaybeRef + /** * Transition duration in milliseconds */ @@ -127,6 +132,7 @@ export function useTransition( ): ComputedRef { const { delay = 0, + disabled = false, duration = 1000, onFinished = noop, onStarted = noop, @@ -188,9 +194,17 @@ export function useTransition( const timeout = useTimeoutFn(start, delay, false) watch(sourceVector, () => { - if (unref(delay) <= 0) start() - else timeout.start() + if (unref(disabled)) { + outputVector.value = sourceVector.value.slice(0) + } + else { + if (unref(delay) <= 0) start() + else timeout.start() + } }, { deep: true }) - return computed(() => isNumber(sourceValue.value) ? outputVector.value[0] : outputVector.value) + return computed(() => { + const targetVector = unref(disabled) ? sourceVector : outputVector + return isNumber(sourceValue.value) ? targetVector.value[0] : targetVector.value + }) } diff --git a/packages/shared/useTimeoutFn/index.spec.ts b/packages/shared/useTimeoutFn/index.test.ts similarity index 100% rename from packages/shared/useTimeoutFn/index.spec.ts rename to packages/shared/useTimeoutFn/index.test.ts From 0fff61b307d85bd0f1aff0d4efe7dc44d2b070c5 Mon Sep 17 00:00:00 2001 From: Anthony Fu Date: Sat, 6 Mar 2021 03:41:27 +0800 Subject: [PATCH 12/84] feat!: introduce `controls` option --- packages/core/useNow/demo.vue | 2 +- packages/core/useNow/index.md | 6 +++- packages/core/useNow/index.ts | 29 +++++++++++++++----- packages/core/useTimeAgo/index.ts | 35 ++++++++++++++++++------ packages/core/useTimestamp/demo.vue | 2 +- packages/core/useTimestamp/index.md | 5 +++- packages/core/useTimestamp/index.ts | 27 ++++++++++++++---- packages/shared/useInterval/index.ts | 41 ++++++++++++++++++++++++---- 8 files changed, 116 insertions(+), 31 deletions(-) diff --git a/packages/core/useNow/demo.vue b/packages/core/useNow/demo.vue index 5aaa087bd86..b6bad170188 100644 --- a/packages/core/useNow/demo.vue +++ b/packages/core/useNow/demo.vue @@ -1,7 +1,7 @@