animation을 사용할 때에는 Timer 함수보다는 requestAnimationFrame
을 사용해야 한다.
js의 메인쓰레드가 아닌 애니메이션 프레임에서 실행되기 때문에 애니메이션 프레임이 소실되거나 여러개의 애니메이션을 실행할 경우에 서로 충돌하는 경우를 방지하기 위해서이다.
최근 숫자가 증가하는 애니메이션을 구현하였는데, 요구사항은 다음과 같다.
- 증가량이 점점 감소할 것(목표치에 가까워지면 조금씩 증가)
- 2초 동안 동작할 것
1번 요구사항때문에 increment라는 변수를 두어 step 함수의 실행마다 step*=0.9
를 해주고 setCurNumber(maxNumber-increment)를 통해 구현하였다.
그리고 2번 요구사항으로 인해 Timer 함수가 아닌 requestAnimationFrame
을 사용하게 되었는데, 이렇게 구현하게 되면 2번 요구사항을 만족할 수 없었다.
requestAnimationFrame
의 호출주기는 주사율에 따라 결정되고 주사율은 다 다르기 떄문에 기기마다 같은 시간동안 호출되는 횟수도 다르고 시간제한을 넣게되면 호출 횟수, 시간, 현재 값의 세 변수에 따라 실행하게 되는데 이러한 점화식을 찾지 못하였다.
결론적으로 말하자면 위에서 말한 점화식(ease함수)를 만들어 가중치를 계산하여 증가속도를 구현하였다.
ease함수는 여기를 참조하였다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| import { useState, useEffect, useRef, useCallback } from "react";
function easeOutExpo(t) { return t == 1 ? 1 : -Math.pow(2, -10 * t) + 1; }
export const useIncreaseNumber = ( maxNumber, initialNumber = 0, duration = 2000 ) => { const [curCount, setcurCount] = useState(initialNumber); const curTime = useRef(null);
const step = useCallback((timeStamp) => { if (curTime.current === null) curTime.current = timeStamp;
const progress = timeStamp - curTime.current;
const nextCount = Math.ceil( (maxNumber - initialNumber) * easeOutExpo(progress / duration) );
const nextCurCount = nextCount > maxNumber ? maxNumber : nextCount;
setcurCount(nextCurCount);
if (progress < duration) { window.requestAnimationFrame(step); } }, []);
useEffect(() => { window.requestAnimationFrame(step); }, []);
return curCount; };
|
주사율을 제한하여 구현하는 아이디어도 생각했는데, 이에 대해서 정리해보려고 한다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61
| import { useState, useEffect, useRef, useCallback } from "react";
function easeOutExpo(t) { return t == 1 ? 1 : -Math.pow(2, -10 * t) + 1; }
export const useIncreaseNumber2 = ( maxNumber, initialNumber = 0, duration = 1000, fpsHz = 1000 / 30 ) => { const [curCount, setcurCount] = useState(initialNumber); const curTime = useRef(null); const lastTime = useRef(null); const cnt = useRef(0);
const step = useCallback((timeStamp) => { if (curTime.current === null && lastTime.current === null) { curTime.current = window.performance.now(); lastTime.current = window.performance.now(); }
const totalElapsed = window.performance.now() - curTime.current;
if (totalElapsed > duration) { return; }
const elapsed = timeStamp - lastTime.current;
if (elapsed >= fpsHz) { const nextCount = Math.ceil( (maxNumber - initialNumber) * easeOutExpo(totalElapsed / duration) );
const nextCurCount = nextCount > maxNumber ? maxNumber : nextCount;
console.log(++cnt.current); setcurCount(nextCurCount); lastTime.current = timeStamp - (elapsed % fpsHz); } window.requestAnimationFrame(step); }, []);
useEffect(() => { window.requestAnimationFrame(step); }, []);
return curCount; };
|
setInterval
의 경우 두 번째 parameter가 호출 주기이기 때문에 반환값을 통해서 쓰로틀링을 충분히 구현할 수 있지만, requestAnimationFrame
의 경우 window.perfomance.now()
를 통해 이를 직접 구현해줘야한다.
상태를 변경하여 브라우저에 변경사항이 반영되는 횟수를 제한하였다.
신경써야 할 점은 lastTime.current 계산식이다.
timeStamp는 마지막 호출의 시간이고 elapsed는 timeStamp와 마지막 상태 변경시간의 차이이다.
상태를 변경하는 조건은 elapsed가 fpsHz보다 클 경우이므로 lastTime을 timeStamp로 하게 되면 elapsed-fpsHz만큼의 값이 축적되어 계산되게 된다.
lastTime.current에 timeStamp를 그냥 저장하게 되면 elapsed-fpsHz만큼의 값이 축적되어 계산되게 된다. 떄문에 lastTime.current에는 timeStamp-(elapsed-fpsHz)를 저장해주어야 한다.
elapsed값은 계속 증가하는 값이기 때문에 나누기 연산으로 계산해주었다.