블로그 내 검색

2011. 12. 21.

자바스크립트의 간단한 애니메이션 함수 구현해보기.

CSS3에서 새로 도입되는 새로운 프로퍼티인 transitiontransform 속성으로 브라우저의 애니메이션 효과는 이제 약간의 학습으로도 구현이 가능하게 되어가고 있다.
(transition의 예제는 신현석 님 블로그를 참고해보자. 잘 정리되어 있다.)
아직 브라우저 지원이 미흡하지만 요새 브라우저의 업데이트 경쟁을 보고 있으면 모든 브라우저에 CSS3이 도입되는건 멀지 않아 보인다.

다만 아직 구형 브라우저나, 기존 구축된 사이트에서는 HTML에 동적인효과를 주기 위해서 아직까지는 javascript가 많이 쓰이고 있는게 현실이고 그것을 유지보수할 때에도 관련 지식은 필요하다. 우리나라에서는 궁극의 최종보스 IE6도 아직 버티고 계시고 말이다.

애니메이션 구현에 대해서 조금 알아보자.

아래는 이번 예제에서 포스팅한 사용자 객체를 사용하여 구현해본 것이다.


나는 작업하면서, 그리고 이 글을 쓰면서 많은 JavaScript 애니메이션 코드를 보았다. JavaScript 애니메이션은 매끄럽지 못하다는 평가를 받는다. 모든 JavaScript 애니메이션은 window.setInterval 메소드를 사용하여 수행된다. 이것은 콜백의 간격과 콜백 함수를 지정하는 타이머 메소드이다. - Jack Herrington (http://www.ibm.com/developerworks/kr/library/x-ajaxslideshow/)


대부분의 javascript 애니메이션은 setTimeout 혹은 setInterval을 사용하여 구현된다.
브라우저에 들어가있는 클라이언트 자바스크립트가 스레드 하나로 구동되기 때문인데, 단일 스레드에서는 반복적인 작업(가령 루프문이라던가...)을 할 경우 다른 작업을 하지 못한다. 한마디로 화면이 굳어버린다는 뜻이다.

어쩔 수 없이 인터벌 류 함수를 사용해야 하는데, 이러한 함수들은 사용에 주의가 많이 필요한건 사실이다. 조심하지 않는다면 자칫 무한루프에 빠지거나, 메모리 릭이 발생한다.

이보다 더 심각한 문제는 시각적인 껄끄러움이다.

자바스크립트의 문제인지, 아니면 브라우저의 문제인지는 모르겠지만 인터벌 류의 함수를 사용하여 구현할 때 인자로 시간을 주게 되어 있는데 이 시간에 따라 애니메이션을 구현할 경우 조금 껄끄러운 움직임을 보여주게 된다.
아무리 시간을 적게 주어도 동작이 뭔가 어색해 보인다는 것인데, 이것은 인자로 주는 시간이 절대 그 시간에 실행하라는 명령이 아닌 제안이기 때문이다.
실제 20밀리초를 줘도 실행은 25초로 실행된다고 한다.

이것을 해결하기 위해서는 다른 방향으로 접근해봐야 한다.
시작시간을 기억해 뒀다가 경과한 시간만큼 진행 프로그레스를 구해서 그만큼을 움직이게 하는 것이다.

현재 애니메이션의 진행율을 검사해서 진행율만큼 속성을 변경시키는, 역으로 접근하는 방식이다.
자바스크립트의 Date 객체에서는 밀리초를 얻을 수 있는 valueOf() 메서드를 제공한다 . 이것으로 구현해 보자.

var javarouka = {

    UI: {
        // 계산된 스타일을 반환하지만, 
        // 스타일이 지정되어 있지 않을 경우, 브라우저별로 수치가 다르게 반환.
        // block 요소의 경우 넓이를 지정하지 않을 경우 
        // 계산된 수치를 반환하기도 하고, auto 값을 반환하는 경우도 있다.
        getStyle: function(ele, styleName) {

            // 스타일이 있다면 반환
            if(ele.style[ele]) {
                return ele.style[styleName];
            }

            // 인터넷 익스플로러 버전
            if(ele.currentStyle) {
                return ele.currentStyle[styleName];
            }

            // W3C 버전
            var st = document.defaultView && document.defaultView.getComputedStyle;
            if(st) {
                styleName = styleName.replace(/([A-Z])/g, "-$1").toLowerCase();
                var cs = document.defaultView.getComputedStyle(ele, "");
                return cs && cs.getPropertyValue(styleName);
            }

            // 별수 없다.
            else {
                return null;
            }
        },

        // CSS를 설정한다.
        css: function(ele, name, point) {
            ele.style[name] = point;
            return ele;
        },

        // top, left, width, height 관련 애니메이션을 처리한다.
        boundsAnimate: function(ele, css, duration, callback) {

            // 애니메이션 시작을 체크한다
            ele.$$isAnimate = true;
            
            // 시작시간 얻기
            var startTime = new Date().valueOf();

            var oldStyle = {}; // 구 스타일
            var newStyle = {}; // 조정 스타일
            var o = n = 0; // 임시 변수

            // 현재 스타일과 이동 스타일을 비교하여 조정스타일을 계산한다. 
            for(var k in css) {
                oldStyle[k] = parseInt(javarouka.UI.getStyle(ele, k), 10);
                if(!oldStyle[k] || oldStyle[k] == "auto") {
                    oldStyle[k] = 0;
                }
                n = parseInt(css[k], 10);
                newStyle[k] = n - oldStyle[k];
            }
            
            // 애니메이션 도중 오버플로우 요소를 노출하지 않는다.
            if(javarouka.UI.getStyle(ele, "overflow") !== "hidden") {
                javarouka.UI.css(ele, "overflow", "hidden");
            }

            // 실제 애니메이션 함수
            function inner() {

                // 지나간 시간 얻기
                if(ele.$$isAnimate) {
                    var passed = new Date().valueOf() - startTime;

                    // 지난 시간동안 진행해야 할 퍼센테이지 얻기
                    // 가령 애니메이션 시간이 최대 1000ms 이고, 
                    // 지난 시간이 100ms초라면
                    // CSS 변경 수치는 10퍼센트만큼 변경되어야 한다.                    
                    var    percent = passed / duration;
                    if (passed <= duration) {
                        for(var k in newStyle) {
                            var changed = 
                               oldStyle[k] + 
                               parseInt(newStyle[k], 10)
                                * percent;
                            javarouka.UI.css(ele, k, changed + "px");
                        }
                        setTimeout(inner, 0);
                    }
                    else {
                        // 종료시간이므로 목표로 했던 스타일을 그냥 적용
                        for(var k in css) {
                            javarouka.UI.css(ele, k, css[k]);
                        }
                        //콜백이 있다면 실행
                        if(typeof callback === 'function') {
                            callback(ele, css);
                        }
                    }
                }
            }
        },

        // 멈춤을 체크한다.
        stop: function(ele, isBack) {
            ele.$$isAnimate = false;
        }
    }
};

이런 식으로 구현하게 되면 브라우저의 딜레이나 랙으로 인해 연산이 느려져도 비교적 정확한 시간에 애니메이션이 종료되며, 종료될 때 목표했던 스타일이 지정되고, 무엇보다 밀리초를 직접 지정하는 방식보다 부드럽게 동작한다.

코드는 주석을 자세하게 첨부하였으니 어렵지는 않을 것이다. 아래는 위의 예제를 사용하는 코드이다.
(포스트 상단에 있는 예제를 동작시킨 코드)
var javaroukaSmaller = function(ele) {
    javarouka.UI.boundsAnimate(
        ele, 
        {
            "left": "50px",
            "width": "150px",
            "height": "150px"
        },
        1000,
        function(ele, css) {
            javaroukaLargger(ele);
        }
    );
};
var javaroukaLargger = function(ele) {
    javarouka.UI.boundsAnimate(
        ele, 
        {
            "left": "0px",
            "width": "300px",
            "height": "200px"
        },
        1000,
        function(ele, css) {
            //javaroukaSmaller(ele);
        }
    );
};
var javaroukaStop = function() {
    javarouka.UI.stop(document.getElementById("box"));
};

var javaroukaStart = function() {
    javaroukaSmaller(document.getElementById("box"));
}
... 그런데 jQuery있으면 다 필요없는데...?

댓글 없음:

댓글 쓰기