Skip to content
This repository has been archived by the owner on Mar 13, 2024. It is now read-only.

Commit

Permalink
fix: multiple animation glitch
Browse files Browse the repository at this point in the history
major refactor in animation-manager engine
  • Loading branch information
davidpa9708 authored and David Perez Alvarez committed Nov 2, 2018
1 parent 8c172b4 commit e385ffd
Show file tree
Hide file tree
Showing 4 changed files with 77 additions and 121 deletions.
108 changes: 27 additions & 81 deletions src/animation-manager.ts
Original file line number Diff line number Diff line change
@@ -1,51 +1,33 @@
import { Animation } from "./animation"
import { EasingFunction } from "./easing"

function almost0(value: number): boolean {
return value < 1 && value > -1
}

function toDirection(horizontal: boolean): "horizontal" | "vertical" {
return horizontal ? "horizontal" : "vertical"
function toDirection(horizontal: boolean): "x" | "y" {
return horizontal ? "x" : "y"
}

interface IHorizontal<T> {
vertical: T
horizontal: T
x: T
y: T
}

export interface Point {
export interface Point extends IHorizontal<number> {
x: number
y: number
}

class AnimationManager {
private shouldBe: Point = { x: 0, y: 0 }
public shouldBe: Point = { x: 0, y: 0 }
private scrollAnimation: IHorizontal<Animation[]> = {
vertical: [],
horizontal: [],
}
private lastPosition: IHorizontal<number> = {
horizontal: 0,
vertical: 0,
x: [],
y: [],
}
private scrollChanged: IHorizontal<number> = {
horizontal: 0,
vertical: 0,
constructor(currentPosition: Point) {
this.shouldBe = currentPosition
}
constructor(
private setPosition: (point: Point) => void,
private external: () => void,
private currentPosition: (horizontal: boolean) => number,
) {}
public stopAllAnimations() {
this.scrollAnimation = {
vertical: [],
horizontal: [],
}
this.scrollChanged = {
horizontal: 0,
vertical: 0,
y: [],
x: [],
}
}
public createScrollAnimation(options: {
Expand All @@ -60,62 +42,26 @@ class AnimationManager {
distToScroll: options.distToScroll,
duration,
easing: options.easing,
stop: () =>
this.scrollAnimation[direction].splice(
this.scrollAnimation[direction].indexOf(animation),
1,
),
})
this.scrollAnimation[direction].push(animation)
if (this.animationsCount === 1) {
this.shouldBe.x = this.currentPosition(true)
this.shouldBe.y = this.currentPosition(false)
window.requestAnimationFrame(this.onAnimationFrame)
}
return animation
}
private get animationsCount() {
return this.scrollAnimation.horizontal.length + this.scrollAnimation.vertical.length
}

private onAnimationFrame = () => {
if (this.animationsCount === 0) {
this.stopAllAnimations()
} else {
if (
!almost0(this.shouldBe.x - this.currentPosition(true)) ||
!almost0(this.shouldBe.y - this.currentPosition(false))
) {
this.external()
}
const distToScroll = this.distToScroll
this.scrollTo({ x: distToScroll.x, y: distToScroll.y })
this.shouldBe = distToScroll
this.scrollChanged.horizontal += this.currentPosition(true) - this.lastPosition.horizontal
this.scrollChanged.vertical += this.currentPosition(false) - this.lastPosition.vertical
window.requestAnimationFrame(this.onAnimationFrame)
}
}
private scrollTo(point: Point) {
this.setPosition(point)
}
private get distToScroll(): Point {
return {
x: this.getDistToScroll(true),
y: this.getDistToScroll(false),
public updateShouldBe() {
const updateShouldBe = (horizontal: boolean) => {
const direction = toDirection(horizontal)
const scrollAnimation = this.scrollAnimation[direction]
scrollAnimation.forEach(
animation => (this.shouldBe[direction] += -animation.distance + animation.updateDistance()),
)
}
}
private getDistToScroll(horizontal: boolean): number {
let distToScroll = 0
const direction = toDirection(horizontal)
this.lastPosition[direction] = this.currentPosition(horizontal)
const initial = this.currentPosition(horizontal) - this.scrollChanged[direction]
const scrollAnimation = this.scrollAnimation[direction]
scrollAnimation.forEach(animation => {
distToScroll += animation.distance
if (animation.isPastAnimation()) {
animation.stop()
this.scrollChanged[direction] -= animation.distance
scrollAnimation.splice(scrollAnimation.indexOf(animation), 1)
}
})
distToScroll += initial
return distToScroll
updateShouldBe(true)
updateShouldBe(false)
return this.shouldBe
}
}

Expand Down
18 changes: 9 additions & 9 deletions src/animation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,31 +4,31 @@ interface ScrollInstanceProps {
duration: number
distToScroll: number
easing: EasingFunction
stop(): void
}

type DOMHighResTimeStamp = number

class Animation {
private initialTime: DOMHighResTimeStamp
private active: boolean = true
public distance: number = 0
public easing: EasingFunction
constructor(private options: ScrollInstanceProps) {
this.initialTime = performance.now()
this.easing = options.easing || defaultEasingFunction
}
public get distance(): number {
return this.isPastAnimation()
public updateDistance(): number {
this.distance = this.isPastAnimation()
? this.options.distToScroll
: this.easing(this.currentDuration, 0, this.options.distToScroll, this.options.duration)
}
public isPastAnimation(): boolean {
return this.currentDuration >= this.options.duration
this.isPastAnimation() && this.stop()
return this.distance
}
public stop() {
this.active = false
this.options.stop()
}
public get isActive() {
return this.active
private isPastAnimation(): boolean {
return this.currentDuration >= this.options.duration
}
private get currentDuration() {
return performance.now() - this.initialTime
Expand Down
20 changes: 3 additions & 17 deletions src/element.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import { onScroll } from "./scroll"

const body = document.body
const html = document.documentElement || {
clientWidth: 0,
Expand Down Expand Up @@ -49,7 +47,8 @@ class ScrollElement {
static isWindow(element: HTMLElement | Window): element is Window {
return element === window
}
constructor(private element: HTMLElement | Window = window) {
constructor(private element: HTMLElement | Window = window, private onScroll?: () => void) {
this.element.addEventListener("scroll", this.scroll)
if (ScrollElement.isWindow(element)) {
this.size = windowSize()
this.scrollSize = windowScrollSize()
Expand All @@ -72,27 +71,14 @@ class ScrollElement {
}
}
}
private _onScroll: onScroll = null
public set onScroll(value: onScroll) {
!!this._onScroll && !!value && this.toggleMount(true)
!this._onScroll && !value && this.toggleMount(false)
this._onScroll = value
}
private scroll = () => {
if (this._onScroll) {
this._onScroll()
}
this.onScroll && this.onScroll()
}
public size: (horizontal: boolean) => number
public scrollSize: (horizontal: boolean) => number
public position: (horizontal: boolean) => number
public offset: (horizontal: boolean) => number
public scrollTo: (x: number, y: number) => void
private toggleMount(add: boolean) {
add
? this.element.addEventListener("scroll", this.scroll)
: this.element.removeEventListener("scroll", this.scroll)
}
}

export { ScrollElement }
52 changes: 38 additions & 14 deletions src/scroll.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import { Animation } from "./animation"
import { AnimationManager, Point } from "./animation-manager"
import { AnimationManager } from "./animation-manager"
import { defaultEasingFunction, EasingFunction } from "./easing"
import { ScrollElement } from "./element"

function almost0(value: number): boolean {
return value < 1 && value > -1
}

export type onScroll = (() => void) | null

type ScrollType = "value" | "percent" | "screen"
Expand Down Expand Up @@ -38,21 +42,29 @@ class Scroll {
private settings: ISettings
private animationManager: AnimationManager
constructor(element?: HTMLElement | Window, settings: Partial<ISettings> = defaultSettings) {
this.element = new ScrollElement(element)
this.animationManager = new AnimationManager(
(point: Point) => {
this.element.scrollTo(point.x, point.y)
const onScroll = () => {
const almostX = almost0(this.animationManager.shouldBe.x - this.element.position(true))
const almostY = almost0(this.animationManager.shouldBe.y - this.element.position(false))
!almostX && (this.animationManager.shouldBe.x = this.element.position(true))
!almostY && (this.animationManager.shouldBe.y = this.element.position(false))
if (almostX && almostY) {
this.settings.onUtilityScroll && this.settings.onUtilityScroll()
},
() => this.settings.onExternalScroll && this.settings.onExternalScroll(),
(horizontal: boolean) => this.element.position(horizontal),
)
} else {
this.settings.onExternalScroll && this.settings.onExternalScroll()
}
this.settings.onScroll && this.settings.onScroll()
}
this.element = new ScrollElement(element, onScroll)
this.animationManager = new AnimationManager({
x: this.element.position(true),
y: this.element.position(false),
})
this.settings = defaultSettings
this.updateSettings(settings)
this.scroll()
}
public updateSettings(settings: Partial<ISettings>) {
this.settings = Object.assign(this.settings, settings)
this.element.onScroll = this.settings.onScroll
this.settings = Object.assign({}, this.settings, settings)
}
public stopAllAnimations() {
this.animationManager.stopAllAnimations()
Expand All @@ -72,23 +84,35 @@ class Scroll {
public scrollTo(scrollType: ScrollType, options: IOptions = this.settings.options) {
const mappedOptions = this.getDefault(options)
const dist = this.getDist(scrollType, mappedOptions.value, mappedOptions.horizontal)
this.offsetScroll({
return this.offsetScroll({
...mappedOptions,
value: dist - this.element.position(mappedOptions.horizontal),
})
}
public scrollBy(scrollType: ScrollType, options: IOptions = this.settings.options) {
const mappedOptions = this.getDefault(options)
const dist = this.getDist(scrollType, mappedOptions.value, mappedOptions.horizontal)
this.offsetScroll({ ...mappedOptions, value: dist })
return this.offsetScroll({ ...mappedOptions, value: dist })
}
private scroll = () => {
const shouldBe = Object.assign({}, this.animationManager.shouldBe)
this.animationManager.updateShouldBe()
if (
shouldBe.x !== this.animationManager.shouldBe.x ||
shouldBe.y !== this.animationManager.shouldBe.y
) {
this.element.scrollTo(this.animationManager.shouldBe.x, this.animationManager.shouldBe.y)
}
window.requestAnimationFrame(this.scroll)
}
private offsetScroll(options: Required<IOptions>) {
return this.animationManager.createScrollAnimation({
const animation = this.animationManager.createScrollAnimation({
distToScroll: options.value,
easing: this.settings.easing,
duration: options.duration,
horizontal: options.horizontal,
})
return animation
}
private getDist(scrollType: ScrollType, value: number, horizontal: boolean): number {
switch (scrollType) {
Expand Down

0 comments on commit e385ffd

Please sign in to comment.