Skip to content

Instantly share code, notes, and snippets.

@seokju-na
Created April 11, 2019 12:56
Show Gist options
  • Save seokju-na/35ff7a3adac4c10fb9ea99fed1dbb823 to your computer and use it in GitHub Desktop.
Save seokju-na/35ff7a3adac4c10fb9ea99fed1dbb823 to your computer and use it in GitHub Desktop.
import { animationFrameScheduler, defer, Observable, range } from 'rxjs';
import { map, takeWhile, tap } from 'rxjs/operators';
/** Observable이 구독된 이후 지난 시간을 rAF 마다 반환합니다. */
export function timeElapsed(scheduler = animationFrameScheduler): Observable<number> {
return defer(() => {
const start = scheduler.now();
return range(0, Number.POSITIVE_INFINITY, scheduler).pipe(map(() => scheduler.now() - start));
});
}
/** 지정된 시간(단위: ms)동안 0-1 까지 반환합니다. */
export function duration(milliseconds: number, scheduler = animationFrameScheduler): Observable<number> {
return timeElapsed(scheduler).pipe(
map(elapsedMilliseconds => elapsedMilliseconds / milliseconds),
takeWhile(timing => timing <= 1)
);
}
export const quarticInOut = (t: number) => (t < 0.5 ? +8.0 * Math.pow(t, 4.0) : -8.0 * Math.pow(t - 1.0, 4.0) + 1.0);
export interface ScrollDownAnimationOptions {
distanceOffset: number;
durationTime: number;
}
/**
* 목적지(distanceOffset)로 지정된 시간(durationTime)(단위: ms) 동안 부드럽게 스크롤
* 합니다.
*
* @example*
* scrollDownAnimation({
* distanceOffset: 500,
* durationTime: 255,
* }).subscribe();
*/
export function scrollDownAnimation(options: ScrollDownAnimationOptions) {
const startOffset = getScrollYOffset();
const direction = options.distanceOffset > startOffset ? 1 : -1;
const distance = Math.abs(options.distanceOffset - startOffset);
return duration(options.durationTime).pipe(
map(quarticInOut),
map(timing => timing * distance),
tap(frame => {
window.scrollTo(0, startOffset + frame * direction);
})
);
}
interface Props {
title: string;
durationTime: number;
className?: string;
}
function ScrollDownButton({ durationTime, title, className }: Props) {
const { animate, cancel } = useScrollDownAnimationBehavior();
const handleClick = useCallback(
() => {
const { height } = getViewportSize();
animate({ distanceOffset: height, durationTime });
},
[durationTime, animate]
);
useEffect(
() => {
const subscription = merge(
fromEvent(window, 'wheel'),
fromEvent(window, 'mousewheel'),
fromEvent(window, 'touchstart')
).subscribe(() => {
cancel();
});
return () => {
subscription.unsubscribe();
};
},
[cancel]
);
return (
<button aria-label={title} onClick={handleClick} className={classNames(css.button, className)}>
<img
alt=""
src="some-image"
aria-hidden="true"
className={css.image}
/>
</button>
);
}
import { useEffect, useMemo, useRef } from 'react';
import { interval, Subject, Subscription } from 'rxjs';
import { switchMap, takeUntil, throttle } from 'rxjs/operators';
import { scrollDownAnimation, ScrollDownAnimationOptions } from 'core-animations.ts';
/**
* 지정된 위치로 부드럽게 스크롤 시킵니다. Throtting이 적용되어 있어 여러번 호출하더라도 애니메이션
* 재생 동안에는 호출을 무시합니다.
*/
export default function useScrollDownAnimationBehavior(): {
animate: (options: ScrollDownAnimationOptions) => void;
cancel: () => void;
} {
const eventStreamRef = useRef(new Subject<ScrollDownAnimationOptions>());
const cancelStreamRef = useRef(new Subject<void>());
const animationSubscriptionRef = useRef(Subscription.EMPTY);
const animate = useMemo(
() => (options: ScrollDownAnimationOptions) => {
eventStreamRef.current.next(options);
},
[]
);
const cancel = useMemo(
() => () => {
cancelStreamRef.current.next();
},
[]
);
useEffect(() => {
animationSubscriptionRef.current = eventStreamRef.current
.pipe(
throttle(({ durationTime }) => interval(durationTime / 2)),
switchMap(options => scrollDownAnimation(options).pipe(takeUntil(cancelStreamRef.current)))
)
.subscribe();
return () => {
animationSubscriptionRef.current.unsubscribe();
};
}, []);
return { animate, cancel };
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment