From c74d4c046759aea15e653f13c2216521472aca1e Mon Sep 17 00:00:00 2001 From: Alec Larson Date: Tue, 17 Sep 2019 19:29:33 -0400 Subject: [PATCH] feat: make "To" and "SpringTransform" inherit from "AnimationValue" These classes don't use much from "SpringValue", so I think we're better off moving the shared functionality into a shared superclass, like "AnimationValue". --- packages/animated/src/AnimationValue.ts | 118 ++++++++++++++++++++++-- packages/core/src/SpringValue.ts | 102 ++++---------------- packages/core/src/To.ts | 80 ++++++++-------- packages/core/src/helpers.ts | 12 +++ packages/shared/src/types/animated.ts | 6 +- targets/web/src/AnimatedStyle.ts | 70 +++++++------- 6 files changed, 221 insertions(+), 167 deletions(-) diff --git a/packages/animated/src/AnimationValue.ts b/packages/animated/src/AnimationValue.ts index 4a61d568f9..4fa16c787a 100644 --- a/packages/animated/src/AnimationValue.ts +++ b/packages/animated/src/AnimationValue.ts @@ -1,19 +1,117 @@ -import { FluidValue, FluidObserver, FluidType, defineHidden } from 'shared' -import { Animated } from './Animated' +import { + FluidValue, + FluidObserver, + FluidType, + defineHidden, + is, + each, +} from 'shared' +import { AnimatedValue } from './AnimatedValue' +import { AnimatedArray } from './AnimatedArray' export const isAnimationValue = (value: any): value is AnimationValue => (value && value[FluidType]) == 2 +/** Called whenever an `AnimationValue` is changed */ +export type OnChange = ( + value: T, + source: AnimationValue +) => void + +/** An object or function that observes an `AnimationValue` */ +export type AnimationObserver = FluidObserver | OnChange + let nextId = 1 -/** @internal A kind of `FluidValue` that provides access to an `Animated` object, a generated identifier, and the current value */ -export abstract class AnimationValue implements FluidValue { - constructor() { +/** + * A kind of `FluidValue` that manages an `AnimatedValue` node. + * + * Its underlying value can be accessed and even observed. + */ +export abstract class AnimationValue + implements FluidValue, FluidObserver { + readonly id = nextId++ + + abstract idle: boolean + abstract node: + | AnimatedValue + | (T extends ReadonlyArray ? AnimatedArray : never) + + protected _priority = 0 + protected _children = new Set>() + + constructor(readonly key?: keyof any) { defineHidden(this, FluidType, 2) } - readonly id = nextId++ - abstract node: Animated - abstract get(): T - abstract addChild(child: FluidObserver): void - abstract removeChild(child: FluidObserver): void + + /** @internal Controls the order in which animations are updated */ + get priority() { + return this._priority + } + set priority(priority: number) { + if (this.priority != priority) { + this.priority = priority + this._onPriorityChange(priority) + } + } + + /** Get the current value */ + get() { + return this.node.getValue() + } + + /** @internal */ + addChild(child: AnimationObserver): void { + if (!this._children.size) this._attach() + this._children.add(child) + } + + /** @internal */ + removeChild(child: AnimationObserver): void { + this._children.delete(child) + if (!this._children.size) this._detach() + } + + /** @internal */ + abstract onParentChange(value: T, idle: boolean, parent: FluidValue): void + + /** @internal */ + onParentPriorityChange(priority: number, _parent: FluidValue) { + // Assume we only have one parent. + this.priority = priority + 1 + } + + protected _attach() {} + protected _detach() {} + + /** Notify observers of a change to our value */ + protected _onChange(value: T, idle = false) { + // Clone "_children" so it can be safely mutated by the loop. + for (const observer of Array.from(this._children)) { + if (is.fun(observer)) { + observer(value, this) + } else { + observer.onParentChange(value, idle, this) + } + } + } + + /** Notify observers of a change to our priority */ + protected _onPriorityChange(priority: number) { + each(this._children, observer => { + if (!is.fun(observer)) { + observer.onParentPriorityChange(priority, this) + } + }) + } + + /** Reset our node and the nodes of every descendant */ + protected _reset(goal?: T) { + this.node.reset(!this.idle, goal) + each(this._children, observer => { + if (isAnimationValue(observer)) { + observer._reset(goal) + } + }) + } } diff --git a/packages/core/src/SpringValue.ts b/packages/core/src/SpringValue.ts index a1b604ff53..66b9a7201a 100644 --- a/packages/core/src/SpringValue.ts +++ b/packages/core/src/SpringValue.ts @@ -6,7 +6,6 @@ import { EasingFunction, toArray, InterpolatorArgs, - FluidObserver, isFluidValue, FluidValue, } from 'shared' @@ -16,6 +15,7 @@ import { AnimatedValue, AnimatedString, AnimatedArray, + OnChange, } from '@react-spring/animated' import invariant from 'tiny-invariant' import * as G from 'shared/globals' @@ -35,7 +35,7 @@ import { } from './runAsync' import { SpringConfig, Animatable, RangeProps } from './types/spring' import { Indexable, Merge } from './types/common' -import { callProp, DEFAULT_PROPS, DefaultProps, matchProp } from './helpers' +import { callProp, DEFAULT_PROPS, DefaultProps, matchProp, isEqual } from './helpers' import { config } from './constants' import { To } from './To' @@ -48,9 +48,6 @@ export type OnAnimate = ( /** Called before the animation is added to the frameloop */ export type OnStart = (spring: SpringValue) => void -/** Called whenever the animated value is changed */ -export type OnChange = (value: T, spring: SpringValue) => void - /** Called once the animation comes to a halt */ export type OnRest = (result: AnimationResult) => void @@ -134,22 +131,15 @@ const BASE_CONFIG: SpringConfig = { clamp: false, } -/** An observer of a `SpringValue` */ -export type SpringObserver = OnChange | FluidObserver - /** An opaque animatable value */ -export class SpringValue - extends AnimationValue - implements FluidObserver { +export class SpringValue extends AnimationValue { static phases = { DISPOSED, CREATED, IDLE, PAUSED, ACTIVE } /** The animation state */ animation?: Animation /** The queue of pending props */ queue?: PendingProps[] /** @internal The animated node. Do not touch! */ - node!: AnimatedNode - /** @internal Determines order of animations on each frame */ - priority = 0 + node!: AnimationValue['node'] /** The lifecycle phase of this spring */ protected _phase = CREATED /** The state for `runAsync` calls */ @@ -160,11 +150,9 @@ export class SpringValue protected _defaultProps: PendingProps = {} /** Cancel any update from before this timestamp */ protected _lastAsyncId = 0 - /** Objects that want to know when this spring changes */ - protected _children = new Set>() - constructor(readonly key: P) { - super() + constructor(key: keyof any) { + super(key) this._state = { key } } @@ -177,13 +165,6 @@ export class SpringValue return this._phase == phase } - /** Get the current value */ - get(): T - get

(prop: P): Animation[P] | undefined - get(prop?: keyof Animation) { - return prop ? this.animation && this.animation[prop] : this.node.getValue() - } - /** Set the current value, while stopping the current animation */ set(value: T, notify = true) { this.node.setValue(value) @@ -380,10 +361,10 @@ export class SpringValue } /** @internal */ - onParentChange(value: any, finished: boolean) { + onParentChange(value: any, idle: boolean) { // The "FrameLoop" handles everything other than immediate animation. if (this.animation!.immediate) { - if (finished) { + if (idle) { this.finish(value) } else { this.set(value) @@ -396,11 +377,6 @@ export class SpringValue } } - /** @internal */ - onParentPriorityChange(priority: number) { - this._setPriority(priority + 1) - } - protected _checkDisposed(name: string) { invariant( !this.is(DISPOSED), @@ -626,27 +602,13 @@ export class SpringValue anim.to = value if (isFluidValue(value)) { value.addChild(this) - this._setPriority((value.priority || 0) + 1) + this.priority = (value.priority || 0) + 1 } else { - this._setPriority(0) - } - } - - protected _setPriority(priority: number) { - if (this.priority == priority) return - this.priority = priority - if (!this.idle) { - // Re-enter the frameloop so our new priority is used. - G.frameLoop.stop(this).start(this) - } - for (const observer of Array.from(this._children)) { - if ('onParentPriorityChange' in observer) { - observer.onParentPriorityChange(priority, this) - } + this.priority = 0 } } - /** @internal */ + /** @internal Called by the frameloop */ public _onChange(value: T, finished = false) { const anim = this.animation if (anim) { @@ -662,25 +624,19 @@ export class SpringValue anim.onChange(value, this) } } + super._onChange(value, finished) + } - // Clone "_children" so it can be safely mutated by the loop. - for (const observer of Array.from(this._children)) { - if ('onParentChange' in observer) { - observer.onParentChange(value, finished, this) - } else if (!finished) { - observer(value, this) - } + protected _onPriorityChange(priority: number) { + // Re-enter the frameloop so our new priority is used. + G.frameLoop.stop(this).start(this) } + super._onPriorityChange(priority) } /** Reset our node, and the nodes of every descendant spring */ protected _reset(goal = computeGoal(this.animation!.to)) { - this.node.reset(!this.idle, goal) - each(this._children, child => { - if (child instanceof SpringValue) { - child._reset(goal) - } - }) + super._reset(goal) } /** Enter the frameloop */ @@ -755,16 +711,6 @@ export class SpringValue return this._getNodeType(value).create(computeGoal(value)) } } - - /** @internal */ - addChild(child: SpringObserver): void { - this._children.add(child) - } - - /** @internal */ - removeChild(child: SpringObserver): void { - this._children.delete(child) - } } // Merge configs when the existence of "decay" or "duration" has not changed. @@ -792,15 +738,3 @@ function computeGoal(value: T | FluidValue): T { })(1) : value } - -// Compare animatable values -function isEqual(a: any, b: any) { - if (is.arr(a)) { - if (!is.arr(b) || a.length !== b.length) return false - for (let i = 0; i < a.length; i++) { - if (a[i] !== b[i]) return false - } - return true - } - return a === b -} diff --git a/packages/core/src/To.ts b/packages/core/src/To.ts index 63abe03b94..5f86d80487 100644 --- a/packages/core/src/To.ts +++ b/packages/core/src/To.ts @@ -1,4 +1,8 @@ -import { AnimatedValue } from '@react-spring/animated' +import { + AnimatedValue, + AnimationValue, + isAnimationValue, +} from '@react-spring/animated' import { createInterpolator, toArray, is, each } from 'shared' import { InterpolatorArgs, @@ -8,14 +12,17 @@ import { Arrify, FluidObserver, } from 'shared/types' - -import { SpringValue } from './SpringValue' +import { isEqual } from './helpers' /** * `To` springs are memoized interpolators that react to their dependencies. * The memoized result is updated whenever a dependency changes. */ -export class To extends SpringValue { +export class To extends AnimationValue + implements FluidObserver { + /** @internal */ + readonly node: AnimatedValue + /** The function that maps inputs values to output */ readonly calc: InterpolatorFn @@ -24,16 +31,13 @@ export class To extends SpringValue { readonly source: OneOrMore, args: InterpolatorArgs ) { - super('to') + super() this.calc = createInterpolator(...args) this.node = new AnimatedValue(this._compute()) - - // Update immediately when a source changes. - this.animation = { owner: this, immediate: true } as any } - protected _animate() { - throw Error('Cannot animate a "To" spring') + get idle() { + return this.node.done } protected _compute() { @@ -43,44 +47,46 @@ export class To extends SpringValue { return this.calc(...inputs) } - /** @internal */ - addChild(observer: FluidObserver) { + protected _attach() { // Start observing our "source" once we have an observer. - if (!this._children.size) { - let priority = 0 - each(toArray(this.source), source => { - priority = Math.max(priority, (source.priority || 0) + 1) - source.addChild(this) - }) - this._setPriority(priority) - } - - super.addChild(observer) + let priority = 0 + each(toArray(this.source), source => { + priority = Math.max(priority, (source.priority || 0) + 1) + source.addChild(this) + }) + this.priority = priority } - removeChild(observer: FluidObserver) { - super.removeChild(observer) - + protected _detach() { // Stop observing our "source" once we have no observers. - if (!this._children.size) { - each(toArray(this.source), source => { - source.removeChild(this) - }) - } + each(toArray(this.source), source => { + source.removeChild(this) + }) } /** @internal */ - onParentChange(_value: any, finished: boolean) { + onParentChange(_value: any, idle: boolean) { + this.node.done = + idle && + // We're not idle until every source is idle. + (idle = toArray(this.source).every( + source => !isAnimationValue(source) || source.idle + )) + // TODO: only compute once per frame - super.onParentChange(this._compute(), finished) + const value = this._compute() + if (!isEqual(value, this.get())) { + this.node.setValue(value) + this._onChange(value, idle) + } } /** @internal */ onParentPriorityChange(_priority: number) { - const reducer = (max: number, source: FluidValue | undefined) => - source ? Math.max(max, (source.priority || 0) + 1) : max - - const max = toArray(this.source).reduce(reducer, 0) - this._setPriority(max) + // Set our priority to 1 + the highest parent. + this.priority = toArray(this.source).reduce( + (max, source) => Math.max(max, (source.priority || 0) + 1), + 0 + ) } } diff --git a/packages/core/src/helpers.ts b/packages/core/src/helpers.ts index 85b9024baa..9187c7f570 100644 --- a/packages/core/src/helpers.ts +++ b/packages/core/src/helpers.ts @@ -10,6 +10,18 @@ declare const process: export const useMemo: typeof useMemoOne = (create, deps) => useMemoOne(create, deps || [{}]) +/** Compare animatable values */ +export function isEqual(a: any, b: any) { + if (is.arr(a)) { + if (!is.arr(b) || a.length !== b.length) return false + for (let i = 0; i < a.length; i++) { + if (a[i] !== b[i]) return false + } + return true + } + return a === b +} + export function callProp( value: T, ...args: AnyFn extends T ? Parameters> : unknown[] diff --git a/packages/shared/src/types/animated.ts b/packages/shared/src/types/animated.ts index 8bd8c69486..0103c37365 100644 --- a/packages/shared/src/types/animated.ts +++ b/packages/shared/src/types/animated.ts @@ -8,9 +8,9 @@ export interface FluidValue { /** @internal An object that observes a `FluidValue` over time */ export interface FluidObserver { - /** A fluid value has changed */ - onParentChange(value: T, finished: boolean, parent: FluidValue): void - /** A fluid value had its priority changed */ + /** An observed `FluidValue` had its value changed */ + onParentChange(value: T, idle: boolean, parent: FluidValue): void + /** An observed `FluidValue` had its priority changed */ onParentPriorityChange(priority: number, parent: FluidValue): void } diff --git a/targets/web/src/AnimatedStyle.ts b/targets/web/src/AnimatedStyle.ts index d6ee47edc1..01f73abc85 100644 --- a/targets/web/src/AnimatedStyle.ts +++ b/targets/web/src/AnimatedStyle.ts @@ -7,8 +7,11 @@ import { isFluidValue, toArray, } from 'shared' -import { AnimatedObject, AnimatedValue } from '@react-spring/animated' -import { SpringValue, SpringObserver } from '@react-spring/core' +import { + AnimatedObject, + AnimatedValue, + AnimationValue, +} from '@react-spring/animated' /** The transform-functions * (https://developer.mozilla.org/fr/docs/Web/CSS/transform-function) @@ -48,7 +51,7 @@ const isValueIdentity = (value: OneOrMore, id: number): boolean => const getValue = (value: T | FluidValue) => isFluidValue(value) ? value.get() : value -type Inputs = (Value | FluidValue)[][] +type Inputs = ReadonlyArray>[] type Transforms = ((value: any) => [string, boolean])[] /** @@ -117,40 +120,17 @@ export class AnimatedStyle extends AnimatedObject { } } -class SpringTransform extends SpringValue { +class SpringTransform extends AnimationValue { + node: AnimatedValue + constructor(readonly inputs: Inputs, readonly transforms: Transforms) { - super('transform') + super() this.node = new AnimatedValue(this._compute()) } - /** @internal */ - onParentChange() { - // TODO: only compute once per frame max - this.set(this._compute()) - } - - /** @internal */ - addChild(observer: SpringObserver) { - // Start observing our inputs once we have an observer. - if (!this._children.size) { - each(this.inputs, input => - each(input, value => isFluidValue(value) && value.addChild(this)) - ) - } - - super.addChild(observer) - } - - /** @internal */ - removeChild(observer: SpringObserver) { - super.removeChild(observer) - - // Stop observing our inputs once we have no observers. - if (!this._children.size) { - each(this.inputs, input => - each(input, value => isFluidValue(value) && value.removeChild(this)) - ) - } + // Required by `AnimationValue` but never used. + get idle() { + return true } protected _compute() { @@ -163,4 +143,28 @@ class SpringTransform extends SpringValue { }) return identity ? 'none' : transform } + + /** @internal */ + protected _attach() { + // Start observing our inputs once we have an observer. + each(this.inputs, input => + each(input, value => isFluidValue(value) && value.addChild(this)) + ) + } + + /** @internal */ + protected _detach() { + // Stop observing our inputs once we have no observers. + each(this.inputs, input => + each(input, value => isFluidValue(value) && value.removeChild(this)) + ) + } + + /** @internal */ + onParentChange() { + // TODO: only compute once per frame max + const value = this._compute() + this.node.setValue(value) + this._onChange(value, true) + } }