Skip to content

Commit

Permalink
Add animation easing
Browse files Browse the repository at this point in the history
  • Loading branch information
j-piasecki committed Sep 4, 2022
1 parent 49057c0 commit 5944494
Show file tree
Hide file tree
Showing 6 changed files with 143 additions and 31 deletions.
1 change: 1 addition & 0 deletions @zapp/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export { StackConfig } from './working_tree/props/StackConfig.js'
export { ColumnConfig } from './working_tree/props/ColumnConfig.js'
export { RowConfig } from './working_tree/props/RowConfig.js'
export { Animation } from './working_tree/effects/animation/Animation.js'
export { Easing } from './working_tree/effects/animation/Easing.js'
export { withTiming } from './working_tree/effects/animation/TimingAnimation.js'
export { Renderer, setViewManager, RenderNode } from './renderer/Renderer.js'
export { ViewManager } from './renderer/ViewManager.js'
Expand Down
12 changes: 11 additions & 1 deletion @zapp/core/src/working_tree/effects/animation/Animation.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import { RememberedMutableValue } from '../RememberedMutableValue.js'
import { Easing } from './Easing.js'

export interface AnimationProps {
onEnd?: () => void
easing?: (t: number) => number
}

export abstract class Animation<T> {
protected static runningAnimations: Animation<unknown>[] = []
Expand All @@ -17,9 +23,13 @@ export abstract class Animation<T> {

protected startTimestamp: number
protected endHandler?: () => void
protected easingFunction: (t: number) => number
protected isRunning = true

constructor() {
constructor(props?: AnimationProps) {
this.endHandler = props?.onEnd
this.easingFunction = props?.easing ?? Easing.linear

this.startTimestamp = Date.now()
Animation.runningAnimations.push(this)
}
Expand Down
79 changes: 79 additions & 0 deletions @zapp/core/src/working_tree/effects/animation/Easing.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
// most of the logic borrowed from https://github.com/software-mansion/react-native-reanimated/blob/3c8f7d013645e77d476004698981b6d526bdb806/src/reanimated2/Bezier.ts

const NEWTON_ITERATIONS = 8
const NEWTON_MIN_SLOPE = 0.001
const SUBDIVISION_PRECISION = 0.001
const SUBDIVISION_MAX_ITERATIONS = 10

function bezier(t: number, p1: number, p2: number): number {
return 3 * (1 - t) * (1 - t) * t * p1 + 3 * (1 - t) * t * t * p2 + t * t * t
}

function bezierSlope(t: number, p1: number, p2: number): number {
return 3 * (1 - t) * (1 - t) * p1 + 6 * (1 - t) * t * (p2 - p1) + 3 * t * t * (1 - p2)
}

function newtonRaphsonIterate(x: number, t: number, x1: number, x2: number): number {
for (let i = 0; i < NEWTON_ITERATIONS; ++i) {
const currentSlope = bezierSlope(t, x1, x2)
if (currentSlope === 0.0) {
return t
}
const currentX = bezier(t, x1, x2) - x
t -= currentX / currentSlope
}
return t
}

function binarySubdivide(x: number, a: number, b: number, x1: number, x2: number): number {
let currentX
let currentT
let i = 0
do {
currentT = a + (b - a) / 2.0
currentX = bezier(currentT, x1, x2) - x
if (currentX > 0.0) {
b = currentT
} else {
a = currentT
}
} while (Math.abs(currentX) > SUBDIVISION_PRECISION && ++i < SUBDIVISION_MAX_ITERATIONS)
return currentT
}

function getTForX(x: number, mX1: number, mX2: number): number {
const initialSlope = bezierSlope(x, mX1, mX2)
if (initialSlope >= NEWTON_MIN_SLOPE) {
return newtonRaphsonIterate(x, x, mX1, mX2)
} else if (initialSlope === 0.0) {
return x
} else {
return binarySubdivide(x, 0, 1, mX1, mX2)
}
}

function makeBezierEasing(x1: number, y1: number, x2: number, y2: number): (t: number) => number {
return (t: number) => {
if (t === 0) {
return 0
}
if (t === 1) {
return 1
}
return bezier(getTForX(t, x1, x2), y1, y2)
}
}

export abstract class Easing {
public static linear(t: number): number {
return t
}

public static ease = makeBezierEasing(0.28, 0.17, 0.27, 1)
public static easeInQuad = makeBezierEasing(0.11, 0, 0.5, 0)
public static easeOutQuad = makeBezierEasing(0.5, 1, 0.89, 1)
public static easeInOutQuad = makeBezierEasing(0.45, 0, 0.55, 1)
public static easeInCubic = makeBezierEasing(0.32, 0, 0.67, 0)
public static easeOutCubic = makeBezierEasing(0.33, 1, 0.68, 1)
public static easeInOutCubic = makeBezierEasing(0.65, 0, 0.35, 1)
}
33 changes: 10 additions & 23 deletions @zapp/core/src/working_tree/effects/animation/TimingAnimation.ts
Original file line number Diff line number Diff line change
@@ -1,46 +1,33 @@
import { Animation } from './Animation.js'
import { Animation, AnimationProps } from './Animation.js'
import { coerce } from '../../../utils.js'

const DEFAULT_DURATION = 300

// TODO: some kind of easing if the performance is not terrible
export interface TimingAnimationProps extends AnimationProps {
duration?: number
}

export class TimingAnimation extends Animation<number> {
private targetValue: number
private duration: number

constructor(targetValue: number, duration: number, onEnd?: () => void) {
super()
constructor(targetValue: number, props?: TimingAnimationProps) {
super(props)

this.targetValue = targetValue
this.duration = duration
this.endHandler = onEnd
this.duration = props?.duration ?? DEFAULT_DURATION
}

public onFrame(timestamp: number): void {
const progress = coerce((timestamp - this.startTimestamp) / this.duration, 0, 1)
this.rememberedValue.value = this.startValue + (this.targetValue - this.startValue) * progress
this.rememberedValue.value = this.startValue + (this.targetValue - this.startValue) * this.easingFunction(progress)

if (progress === 1) {
this.onEnd()
}
}
}

export function withTiming(targetValue: number): TimingAnimation
export function withTiming(targetValue: number, duration: number): TimingAnimation
export function withTiming(targetValue: number, onEnd: () => void): TimingAnimation
export function withTiming(targetValue: number, duration: number, onEnd: () => void): TimingAnimation
export function withTiming(targetValue: number, durationOrEnd?: number | (() => void), onEnd?: () => void) {
if (onEnd !== undefined && durationOrEnd !== undefined) {
return new TimingAnimation(targetValue, durationOrEnd as number, onEnd)
} else if (durationOrEnd !== undefined) {
if (typeof durationOrEnd === 'number') {
return new TimingAnimation(targetValue, durationOrEnd)
} else {
return new TimingAnimation(targetValue, DEFAULT_DURATION, durationOrEnd)
}
} else {
return new TimingAnimation(targetValue, DEFAULT_DURATION)
}
export function withTiming(targetValue: number, props?: TimingAnimationProps) {
return new TimingAnimation(targetValue, props)
}
3 changes: 2 additions & 1 deletion watch-test/page/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
Alignment,
Arrangement,
EventManager,
Easing,
} from '@zapp/core'

Page({
Expand All @@ -31,7 +32,7 @@ Page({
Column(Config('column').fillWidth(0.75).fillHeight(0.75).background(0xff0000).padding(10), () => {
const weight = remember(1)
sideEffect(() => {
weight.value = withTiming(2, 3000)
weight.value = withTiming(2, { duration: 3000, easing: Easing.easeOutQuad })
})
Stack(
StackConfig('row1')
Expand Down
46 changes: 40 additions & 6 deletions web-test/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
Alignment,
Arrangement,
RowConfig,
Easing,
} from '@zapp/core'
import { NavBar, RouteInfo } from './NavBar'
import { Page } from './Page'
Expand Down Expand Up @@ -227,12 +228,45 @@ function AnimationExample() {
.fillSize()
.padding(50, 0),
() => {
const x = remember(0)
const x = remember(-250)
const target = remember(250)
const easing = remember(Easing.linear)

Row(Config('animation-chooser-one'), () => {
Button(Config('btn-animation-linear'), 'Linear', () => {
easing.value = Easing.linear
})
Button(Config('btn-animation-ease'), 'Ease', () => {
easing.value = Easing.ease
})
})
Row(Config('animation-chooser-two'), () => {
Button(Config('btn-animation-inquad'), 'EaseInQuad', () => {
easing.value = Easing.easeInQuad
})
Button(Config('btn-animation-outquad'), 'EaseOutQuad', () => {
easing.value = Easing.easeOutQuad
})
Button(Config('btn-animation-inoutquad'), 'EaseInOutQuad', () => {
easing.value = Easing.easeInOutQuad
})
})
Row(Config('animation-chooser-three').padding(0, 0, 0, 70), () => {
Button(Config('btn-animation-incubic'), 'EaseInCubic', () => {
easing.value = Easing.easeInCubic
})
Button(Config('btn-animation-outcubic'), 'EaseOutCubic', () => {
easing.value = Easing.easeOutCubic
})
Button(Config('btn-animation-inoutcubic'), 'EaseInOutCubic', () => {
easing.value = Easing.easeInOutCubic
})
})
Stack(Config('stack').width(100).height(100).background(0x00ff00).offset(x.value, 0))
Stack(Config('spacer').height(100))
Stack(Config('spacer').height(50))
Button(Config('btn-animate'), 'Animate', () => {
x.value = withTiming((Math.random() - 0.5) * 500, 500)
x.value = withTiming(target.value, { easing: easing.value, duration: 1000 })
target.value = target.value * -1
})
}
)
Expand All @@ -246,16 +280,16 @@ function DynamicLayoutExample() {
const position = remember({ x: 0, y: 0 })

sideEffect(() => {
padding.value = withTiming(100, 2000)
padding.value = withTiming(100, { duration: 2000 })
})

Column(Config('col').fillSize().padding(padding.value).background(0x000000), () => {
const weight = remember(2)
const size = remember(50)

sideEffect(() => {
weight.value = withTiming(1, 3000)
size.value = withTiming(200, 3000)
weight.value = withTiming(1, { duration: 3000 })
size.value = withTiming(200, { duration: 3000 })
})

Row(Config('row1').fillWidth(1).weight(1), () => {
Expand Down

0 comments on commit 5944494

Please sign in to comment.