Views 와 Widgets사이에 커플링을 막고자 사용함.
뷰의 상하의 간에 메시지를 주고 받고 해야 한다.
일반적인 옵저버 패턴.


Broadcasting and Listening

eventHandlerA.on('A', function(data){ alert(data.msg); });  // alerts 'ALERT!' 

eventHandlerA.emit('A', message);//trigger

emit나 trigger나 같지만, emit는 밖에서 호출할 때, trigger는 안에서 호출할 때.


Piping
이벤트를 아래로 흐르게 해준다.(like 캡처링)

eventHandlerA.pipe(eventHandlerB).pipe(eventHandlerC);
eventHandlerC.on('A', function(data){alert(data.msg)});  // alerts 'ALERT!'
eventHandlerA.emit('A', message);


Subscribing

이벤트를 위로 올려준다.(like 버블링)

eventHandlerC.subscribe(eventHandlerB);
eventHandlerB.subscribe(eventHandlerA);
eventHandlerC.on('A', function(data){alert(data.msg)});  // alerts 'ALERT!'
eventHandlerA.emit('A', message);


Filtering

이벤트를 중간에 필터링 해줌.

var myFilter = new EventFilter(function(type, data) {
    return data && (data.msg === 'ALERT!'); //true or false
});

eventHandlerA.pipe(myFilter).pipe(eventHandlerB);
eventHandlerB.on('A', function(data){
    alert('piped message: ' + data.msg);
});



Mapping 

라우팅하듯이 특정 eventHandler에 이벤트를 전달한다.(개인적으론 불필요하다고 생각)

var myMapper = new EventMapper(function(type, data) { return (data && (data.direction === 'x')) ? eventHandlerB : eventHandlerC; });

eventHandlerA.pipe(myMapper); eventHandlerB.on('A', function(data){ alert('B direction : ' + data.direction); }); eventHandlerC.on('A', function(data){ alert('C direction : ' + data.direction); }); eventHandlerA.trigger('A', {direction : 'x'}); // pipes to eventHandlerB eventHandlerA.trigger('A', {direction : 'y'}); // pipes to eventHandlerC

pipe만 가능하다.(당연한다 subscribe가 되려면 mapping에서 이벤트를 받아야 함)


Arbitration

Mapping하고 비슷한 느낌인데 mode별로 이벤트를 등록할 수 있다.
한번에 하나의 모드만 가능하고 setMode로 하면 해당 모드만 동작.
이것도 왜 있어야 하는지 모르겠다.


var eventArbiter = new EventArbiter();

eventArbiter.forMode('routeA').on('A', function(data){
    alert('subscribed message: ' + data.msg);
});
eventArbiter.forMode('routeB').on('B', function(data){
    alert('subscribed message: ' + data.msg);
});

eventArbiter.setMode('routeA');

eventArbiter.forMode('routeA').emit('A', message); // alerts 'ALERT!'
eventArbiter.forMode('routeB').emit('B', message); // does nothing. Mode is not set.

eventArbiter.setMode('routeB');

eventArbiter.forMode('routeA').emit('A', message); // does nothing. Mode is not set.
eventArbiter.forMode('routeB').emit('B', message); // alerts 'ALERT!'


Event Handling Inside a Widget

자체적으로 몇가지 룰이 있다.

External to a Widget:

widget.trigger : the interface to talk to a widget

widget.on : the interface to listen to a widget

widget.pipe : the interface to pipe from a widget

widget.subscribe : the interface to subscribe from a widget


Internal to a Widget:

receive events via widget.eventInput

broadcast events via widget.eventOutput

widget.emit는 없음.


//아래와 같이 인풋 핸들러를 등록.

EventHandler.setInputHandler(widget, eventHandlerA);
eventHandlerA.on('B', function(data){alert(data.msg)});
widget.trigger('B', message);


Listening

setInputHandler으로 등록한 놈이 받음

// Child widget
function Child(){
    // setup input and output handlers
    this.eventOutput = new EventHandler();
    this.eventInput = new EventHandler();
    EventHandler.setInputHandler(this, this.eventInput);
    EventHandler.setOutputHandler(this, this.eventOutput);

    this.eventInput.on('hires tutor', function(){
        alert('Accepted to Harvard');
    }.bind(this));
}



// Parent widget
function Parent(){
    // setup input and output handlers
    this.eventOutput = new EventHandler();
    this.eventInput = new EventHandler();
    EventHandler.setInputHandler(this, this.eventInput);
    EventHandler.setOutputHandler(this, this.eventOutput);

    this.child = new Child();
    this.eventInput.on('bad report card', function(){
        this.child.trigger('hires tutor');
    }.bind(this));
}

var parent = new Parent();
parent.trigger('bad report card');


Broadcasting

setOutputHandler으로 등록한 얘가 전달.

EventHandler.setInputHandler(widget, eventHandlerA);
eventHandlerA.on('B', function(data){alert(data.msg)});
widget.trigger('B', message);



보면서 느낀건 전체적으로 과한 느낌이 있다.

너무 많다. 이런 경우 나중에 이벤트로만 넣으면 데이터가 흘러갈 때 문제가 있을 것 같다.
특히 mapping은 불필요. 데이터가 deep copy되지 않기 때문에 중간에 값을 바꾸면 디버깅의 거의 불가능하고 경험상 이벤트 종류는 너무 할 정도로 간단하게 가야 한다. 복잡하게 되면 나중에 엄청 고생함.

Posted by 전용우

Transition의 현재 상태를 반환하는 객체다.
단독적으로 사용하지 않고 Surface나 Modifier에 붙여서 사용

var state = new Transitionable(0);
state.set(100, {duration : 500});//바로 실행
state.get(); //현재 상태 반환

[x,y,z]좌표값을 넣을 수 있다.
마지막 인자로 함수를 넣으면 콜백이 실행됨.

var state = new Transitionable(0);
state.set(100, {duration : 500}, function(){ alert('done!'); });

즉, Surface나 Modifier에서 set을 호출하고 get을 호출하여 값을 받아 움직인다고 생각하면 될 것 같다.


Transition은 Tween, Physics으로 두개가 있다. 

Tween Transitions

간단하게는 위에처럼 하고 30개의 curve을 제공한다.

var state = new Transitionable(0);
state.set(100, {duration : 500, curve : 'easeInOut'});

혹은 기본적으로 들어 있는 경우가 아닌 경우 직접 아래와 같이 등록해서 사용가능.
물론 만드는 것도 가능.

var TweenTransition = require('famous/transitions/TweenTransition');
TweenTransition.registerCurve('inSine', Easing.inSine);

//ex)
var customCurve = function(t){ return Math.pow(t,2); };
var TweenTransition = require('famous/transitions/TweenTransition');
TweenTransition.registerCurve('custom', customCurve);

Physics Transitions

가속도가 붙은 물리 transition이다.

SpringTransition

- 점을 넘어서 바운스

WallTransition

- 점을 안쪽에서 바운스

SnapTransition

- 스프링과 비슷한데 damping이 항상 들어감.


Tweens vs Physics

정확한 값이 아니고 정확한 타이밍에 이벤트를 발생시켜야 하는게 아니지만, 사용자 이벤트에 따라 변경하고 싶다면 사용한다.



Posted by 전용우


famo.us 코드 분석글은 정확한 글이 아니라 공부하면서 정리한 글이라 정확하지 않고 개인적인 의견들이 난무합니다. 정리하는 용도이니 추후 정리가 완료되면 좀 더 정확한 내용을 올릴 예정이니 나중에 정리된 글을 보세요.


앞에선[링크] famo.us에서 Render Tree를 알아봤고 이번에는 Render Tree의 레이아웃을 어떻게 하는지 알아본다.

앞에서 봤듯이 DOM Tree가 아니라 자체 Render Tree을 가지고 있고 레이아웃도 일반적으로 CSS로 하는게 아니라 famo.us에서 제공하는 레이아웃 방법을 사용한다.

레이아웃하는 방법은 Transforms와 Modifiers을 사용하면 된다.


Transforms

Transforms는 이름에서 나오는 느낌처럼 이미지처럼 걍 CSS3을 Transform을 자바스크립트로 옮긴 객체라고 생각하면 된다.

Transforms은 정적인 객체로 쉽게 translate, scale..을 관리할 수 있다. 내부적으로 css의 matrix3d을 사용하기 때문에 4X4 matrix 형식으로 다루는데 16개의 원소가 있는 배열로 관리한다.

Transforms은 아래와 같은 메서드들을 가지고 있고 아래와 같이 반환한다.


다른 메서드들이야 다를건 없는데 inFront, behind는 css의 z-index처럼 노드의 상하를 조절할 때 사용한다. 내부적으론 translateZ을 걍 살짝 추가한다.

Transform는 그냥 사용되지 않고 Scene Graph(보이는 노드들)에 붙여 이동할 때 사용한다.

var surface = new Surface({
        size: [50, 50],
        properties: { background: 'red' }
    });

    var modifier = new Modifier({
        transform : Transform.translate(100, 100, 0)
    });

    context.add(modifier).add(surface);

그리고 아래와 같이 다수개의 Transform들을 합치거나 기존의 Transform에 추가하여 사용하곤 한다.



Modifiers

Transform은 사실 현재 상태를 나타내는 객체로 혼자서 움직이거나 하지 못한다. 위에서 본 예제처럼 Modifiers에 붙여 Scene Graph에 붙여 사용한다. 그리고 Modifiers는 기본적으로  사이즈나 크기등을 조절할 수 있다.

크기의 경우, 아래와 같이 Surface에 size을 undefined로 하고 Modifirer에 지정하면 undefined의 경우 Surface에 사용한다. 만약 Surface의 값이 다 가지고 있다면 Modifiers의 값을 사용하고 있지 않다. 그래서 크기가 유동적이라면 Surface에서 등록하지 않고 Modifiers에서 등록하면 될 것 같다.


  var sizeModifier = new Modifier({size: [200, 200]});

  var surface = new Surface({
    size: [undefined, 100],
    properties: { background : 'red' }
  });

  context.add(sizeModifier).add(surface);


정렬의 경우, 문서에는 Origin,Align의 방법이 있는데 실제 코드에는 origin밖에 없다.

문서가 좀 더딘듯. origin은 몇가지가 지원되며 아래와 같다.



잼있게도 DOM을 직접 움직인다면 margin으로 처리했을 수 있는데 모두 계산하여 translate로 이동한다.


레이아웃을 사용할 때는 Modifiers을 사용한다고 했는데 이게 두가지 방법이 있다.

하나는 StateModifier, 다른 하나는 Modifier이다.

StateModifier는 push-based라고 하고 Modifier는 pull-based라고 한다.

말은 어렵지만, StateModifier는 내부적으로 모든 Transitionable(Translate...)들이 있어 Modifier에 설정하여 넣으면(push) 된다.

var surface = new Surface({
    size: [70, 70],
    properties: { background: 'red' }
});

var stateModifier = new StateModifier({
    opacity: 1
});

context.add(stateModifier).add(surface);

// animate the opacity from 1 to 0 over 500ms using a linear easing curve
stateModifier.setOpacity(
    0,
    {curve: 'linear', duration : '500'},
    function() { console.log('animation finished!') }
);


Modifier는 내부적으로 Transitionable가 없는 빈 상태로 아래와 같이 Transitionable을 연결하여 Modifier에서 Transitionable의 값을 가져와서(pull) 적용할 수 있다.


var surface = new Surface({
    size: [70, 70],
    properties: { background: 'red' }
});

var modifier = new Modifier();

context.add(modifier).add(surface);

// the opacityGetter
var opacityState = new Transitionable(1);

modifier.opacityFrom(opacityState);

// animate the opacity from 1 to 0 over 500ms using a linear easing curve
opacityState.set({
    Transform.translate(100, 0, 0),
    {curve : 'linear', duration : 500},
    function(){ console.log('animation finished!'); }
});

 

이번에는 레이아웃하는 방법등을 알아봤다, 좀더 자세한 내용은 아래 링크를 참고하면 된다.


https://famo.us/guides/layout

Posted by 전용우

famo.us 코드 분석글은 정확한 글이 아니라 공부하면서 정리한 글이라 정확하지 않고 개인적인 의견들이 난무합니다. 정리하는 용도이니 추후 정리가 완료되면 좀 더 정확한 내용을 올릴 예정이니 나중에 정리된 글을 보세요.



매주 화요일날 만나서 관심있는 자바스크립트 라이브러리를 분석하는 스터디를 진행 중인데 난 famo.us을 한다. 예전에 facebook에 paper을 웹으로 만들면서 화제가 됬고 최근데 2000만불을 펀딩 받은 것으로 유명하다. 

회사얘기는 대충하고 일단 이 친구들이 말하는게 다양한 폰에서 고퀄의 애니메이션을 보장해준다고 하길래 모바일웹을 개발해 본 입장에서 이게 가능할까라는 생각이 들어서 시작한다.

이번에는 프로젝트의 환경은 어떤지 찾아보고 몇 가지 문서를 보고 구조를 파악했다.


환경

빌드하는 도구가 있을 것 같지만, package.json에는 lint랑 jscs만 한다. 머징한 파일이 있는 것으로 보아 내부적으로 관리하는 것 같다. 컨트리뷰트 과정을 확인해보니 테스트 코드에 대한 내용이 없는데 내부적으로는 어떨지 모르겠다. 그리고 requirejs로 의존성 관리를 한다.


실행

기본적으로 실행하기 위해 몇 가지가 필요하다.

1. famous code : 내가 찾아볼 파일

2. polyfill code : 일부 polyfill들

3. css : 기본 스타일이나 크게 많지 않고 일부다.


구조

이번 내용은 주로 [링크]을 보고 정리한 내용. 

famous는 자체적으로 브라우저와 같은 Render Tree의 형식을 가지고 있다. 그래서 60fps를 가능하게 만들어준다.(왜 그런지는 끝에) famous는 JS로만 개발가능하게 만든 라이브러리로 sencha와 비슷한 컨셉

먼저, Context를 만듬. 브라우저로 따지면 root라고 생각하면 됨.

var context = Engine.createContext();
context.add(surface);

브라우저의 RenderObect와 같이 다수의 node을 가진다.

node의 종류는 Renderables, Modifiers가 있다.

Renderables은 실제 화면에 그려주는 역할을 하는 node로 ContainerSurface, ImageSurface, InputSurface, CanvasSurface, VideoSurface가 있다. 걍 엘리먼트라고 생각하면 된다. 여기서는 surface가 렌더링 되는 최소의 단위고 직접 컨트롤하기 보다는 surface를 통해하기를 말한다.

Modifiers는 움직이는 역할이다. Renderables은 정적인 컨텐츠고 실제로 움직일 때는 Modifiers을 사용한다.

var context = Engine.createContext(); var chain = context.add(modifier); chain.add(surface); var modifier = new Modifier({ transform : Transform.translate(100,200) });


그리고 각 노드들은 트리 형식의 구조를 만들 수 있고 add메서드을 통해 Tree을 만든다.

Chaining Nodes : 
  context                    var context = Engine.createContext();
     │
 modifier1                   context.add(modifier1)
     │                              .add(modifier2)
 modifier2                          .add(surface);
     │
  surface

Branching Nodes : 
      context                var context = Engine.createContext();
   ┌─────┴─────┐
modifier    surface2         context.add(modifier).add(surface1); // left branch
   │
surface1                     context.add(surface2);               // right branch

Views라는 개념이 있는데 이건 그냥 일반적으로 얘기하는 UI컴포넌트의 형식이라고 생각하면 된다. Render Tree에 추가하고 이벤트를 받거나 전달하고 파라메터, 상태를 지정가능. 여기서는 shadow dom과 같은 컨셉이라고 얘기한다. 아마도 직접 돔을 컨트롤하지 않고 View을 컨트롤하기 때문에 그런듯.

정리하면, Surfaces는 컨텐츠, Modifiers는 layout, 구조는 add메서도로 연결.



 Render Tree와 DOM을 비교한 표.

보면 알겠지만, famo.us는 html을 사용하는게 아니라 모두 famo.us객체를 사용한다. 

얻는 장점은 모든지 컨트롤 가능하다. 즉, 돔을 직접 수정한다면 생기는 문제점을 famo.us는 requestAnimationFrame에서 batch로 돌려 개선할 수 있다.

물론 단점은 생각보다 협업하기가 까다롭고, 모든걸 famo.us을 믿고 가야 하기 때문에 쉽지 않은 방법이다.


일단, 여기까지 본 결과 성능이 좋은 이유는 famo.us는 직접 DOM을 컨트롤하지 않고 Render Object을 만들어 컨트롤하기 때문에 requestAnimationFrame으로 batch를 하기 때문이 아닐까 생각하고 있다. 좀 더 살펴보는걸로.


https://famo.us/guides/render-tree


Posted by 전용우