실버라이트 애니메이션 모델에서 가장 아쉬운 점 세 가지를 들어보자면,
1. Path를 따라가는 애니메이션 작성 불가.
2. 임의의 스토리보드를 거꾸로 재생 불가.
3. 스토리보드의 특정 키프레임 혹은 특정 시점에서 발생되는 이벤트 부재.
정도에요.

이 중에서 스토리보드를 거꾸로 재생하는 것은 어느 정도 제한이 있긴 하지만 상당히 간단한 코드로 가능해요.
피터씨가 포스팅한
10. Expression Blend_ListBox 간지나게 보이기
11. Expression Blend_Menu에서 스토리보드를 거꾸로 재생하는 프로토타입 코드가 있었죠.

우선 동작 완구부터...

별거 없어요. 그냥 Forward하면 정방향으로, Reverse하면 역방향으로 애니메이션이 진행되는 거죠.
위 정도의 애니메이션이라면 그냥 노가다로 거꾸로 진행되는 걸 하나 만들어도 그만이죠.
그러나 애니메이션에 들어가는 키프레임이 수백개가 넘는다면? 악몽이죠. 그걸 거꾸로 돌리고 싶다면...

역방향 재생을 어떻게 할 수 있을지 간단하게 정리해 보자면...
1. 실버라이트는 스토리보드를 거꾸로 재생하는 메서드는 제공하지 않지만 AutoReverse 속성을 true로 설정하면 한번은 정방향으로 재생하고 그 직후 다시 역방향으로 재생할 수 있어요.
2. 또한 실버라이트의 스토리보드는 Seek 메서드를 사용하여 특정 시간으로 점프할 수 있어요.
3. 그러므로 역방향으로 재생할 때에는 스토리보드의 AutoReverse 속성을 true로 설정한 후 스토리보드를 처음부터 시작하고 곧바로 Seek메서드를 호출하여 스토리보드의 총 길이 만큼 이동하면 그 직후 AutoReverse에 의해 스토리보드가 거꾸로 가게 되죠.

이 외에도 범용적으로 사용하기 위해 스토리보드의 실제 총 길이를 계산하는 메서드도 필요하고 정방향으로 재생할 때에는 AutoReverse에 의해 역방향으로 재생되지 않도록 적절한 시점에 Pause를 걸어주는 처리도 필요하죠.

이 모든 처리를 하나의 클래스(ReverseStoryboard)에서 사용할 수 있도록 만들어봤어요.
소스 코드를 다운로드해 보시면 쉽게 이해할 수 있을거에요.

사용법은 간단해요.
// 타겟 스토리보드를 가지고 새 인스턴스 생성
private ReverseStoryboard rs = new ReverseStoryboard(TargetStoryboard);
rs.BeginForward();  // 정방향으로 재생
// 또는...
rs.BeginReverse();  // 역방향으로 재생
게다가 빌트인 Storyboard처럼 Completed 이벤트도 지원하고 Completed 이벤트에서 정방향 재생이었는지 역방향 재생이었는지를 알 수 있어요.
참 쉽죠? ;)

저작자 표시 동일 조건 변경 허락
신고
Posted by gongdo

왜 파워포인트 같은 데에서 애니메이션 만들다 보면 시계방향으로 보여주기 같은 거 있죠?
한번 실버라이트로 만들어봤는데 스토리보드만으로 만드는 것은 영 마음에 들지 않더군요.

우선 작동 완구와 소스 코드 부터…

시계 방향으로 회전하는 클리핑 애니메이션을 구현 하는 방법은 여러가지가 있는데요, 그 중에서 저는 삼각형 4개를 사용하여 순차적으로 삼각형의 한 꼭지점이 이동하는 방식을 사용했어요.

Clip 속성을 PathGemetry로 설정하고 그 안에 4개의 삼각형 Path를 통해 목표 엘리먼트를 사분면으로 분할했죠. 만약 특정 사분면을 완전히 보이지 않게 하려면 삼각형의 한 꼭지점을 다른 꼭지점과 겹쳐놓으면 되고 완전히 보이게 하려면 한 꼭지점을 두 선분이 직각을 이루도록 옮기면 되죠.

위의 작동 완구를 보시면 알 수 있듯이 왼쪽의 애니메이션은 클리핑의 진행속도가 일정하지 않은 것을 알 수 있어요. 왼쪽의 애니메이션은 다음과 같은 방법으로 작동해요.

1사분면에 있는 삼각형이 단순하게 첫번째 꼭지점에서 두번째 꼭지점으로 직선적으로 이동을 하는거죠.

이 방법의 장점은 오직 블렌드만 가지고도 비교적 간단하면서도 효과적으로 시계 방향 회전 애니메이션을 구현할 수 있다는 것이고요, 단점은 앞서 얘기한 것처럼 중앙부분에서 가장 빠르고 양 끝에서 살짝 느려지는 현상이에요.

이 문제를 해결하려면 다음과 같이 삼각형의 꼭지점이 원궤도를 따라서 이동하게 해야 하죠.

OnEnterFrame, Gameloop와 같은 FPS엔진을 하나 돌리고 매 프레임 마다 특정 좌표(중심점)를 기준으로 회전하는 좌표를 계산하여 각 삼각형을 이루는 클리핑 Path의 점을 이동하면 되죠. 공식은… 솔직히 수학에 젬병이라 모르겠고 ‘회전 좌표 계산’등으로 검색하면 공식과 함께 매우 간단한 소스코드도 널려 있어요. 허접하지만 제 코드를 참고할 수도 있겠고요.

물론 후자가 정확한 속도로 움직이므로 더 완벽하다고 할 수 있겠지만 추가적인 코드 작업이 필요하다는 단점이 있죠. 그래서 간단하게 ContentControl로 시계 방향으로 회전하면서 클리핑하는 패널을 만들어 봤어요. 사용 법도 간단해서 XAML에서도 사용 가능하죠.

<CircularClipper Duration="00:00:04" IsActivated="True">
    <Grid>
        <!—넣고 싶은 모든 종류의 비주얼 -->
    </Grid>
</CircularClipper>

그렇지만 딱 한번 쓰고 말거라면 그냥 스토리보드로 만드는 것도 나쁘지 않을 것 같아요. 무엇보다 머리 안쓰고 금방 만들 수도 있으니까요.

언제나 그렇지만, 테스트 해 보시고 더 좋은 방법이 있다면 공유해보아요~ ^^

저작자 표시 동일 조건 변경 허락
신고
Posted by gongdo

http://cafe.naver.com/mssilverlight/3021에 니르워프님이 결정적인 정보와 함께 실험 설계의 문제점도 짚어주셨어요.

[이슈1] 상이한 측정 결과

제 컴에서 공도님의 소스를 받아서 실행을 해본바, 이슈1일때 공도님의 블로그에서 보여 지는 그런식의 결과는 없다라는 것입니다. MaxFramerate의 설정과 무관하게, 주어진 인터벌로 나옴.

16ms의 인터벌을 주면, MaxFramerate의 설정에 따라 MaxFramerate만큼으로 제한되는 프레임 레이트를 출력하는것이 공도님의 블로그에 올려져 있으나 저의 측정에서는 모두 16ms에 해당하는 프레임 레이트로 나옴.

소스 분석해본바, 공도님의 방법은 MaxFramerate과의 연관성이 파악하는 방법이 아니어보이네요.

[이슈2]

적절치 않는 계산 방법을 사용하고 있습니다.

하나의 인터벌이 지나면(완료되면) 한개의 카운팅을 하는 방법을 취하고 있고, 인터벌이 완료되었을때 할당된 시간이 초과되었는지를 점검하는데, 이 방법으로 하면, 할당 시간보다 조금더 지나칩니다. 인터벌이 클수록 지나치는 크기는 더 커질수 있습니다.

인터벌이 50이라면 49이상의 오차도 나올 수 있습니다.

일단 이슈2를 해결하기 위해 코드를 다음과 같이 약간 수정했고요,

private readonly int LoopCount = 100;

void onTime(object sender, EventArgs e)

{

    if (++_count == LoopCount)

    {

        TimeSpan elapsed = DateTime.Now - _beginTime;

        StopStoryboard();

        StopTimer();

        outputText.Text = string.Format("{0} FPS", ((LoopCount / elapsed.TotalMilliseconds) * 1000).ToString("0.00"));

    }

}

작동 완구와 소스는 아래... 또는 여기에서 테스트. http://shiverlight.net/Sample/GameLoopTest/




문제는 결과에 큰 차이가 없다는 점이에요.

이렇게 되면 짐작해 볼 수 있는 건 현재 운영체제의 버전이나 개별 상황에 따라 실버라이트 런타임이 제공하는 타이머의 안정도나 정확도가 떨어질 수 있느냐의 문제인데요, 각자 테스트 조건을 다르게 해서 특이할 만한 사항이 있으면 알려주시길 바래요.

제 조건은 아래와 같아요.

OS : Vista Enterprise SP1
CPU : Core2Duo 1.4GHz
GPU : Intel GMA950
Browser : IE7, FF3

OS와 브라우저, CPU, GPU외에 영향을 줄 만한 요소는 없을 거라고 생각하고요. 램이야 남을 만큼 남아 있으니^^;;

어쨌거나, 좋은 조언을 해주신 니르워프님께 감사드려요 :)

저작자 표시 동일 조건 변경 허락
신고
Posted by gongdo

결론부터 얘기하자면 스토리보드든 타이머든 정밀도는 서로 같았으며, 양쪽다 타이머의 최소 인터벌에 따라 MaxFramerate와 관계 없이 상당히 부정확한 Tick을 얻게 되는군요. 또한 일부 인터벌의 경우 특정 FPS에서 납득하기 어려운 결과가 나오기도 하는데요, 이렇게 되면 뭔가 실험 설계를 잘못한 게 아닌가 하는 생각마저 드네요.

우선 작동 완구와 소스부터...




실험 설계

간단해요. 우선 실버라이트 플러그인의 Settings 속성을 통해 MaxFramerate를 설정하여 최대 FPS를 제한하고 빈 Storyboard를 이용한 게임루프와 DispatcherTimer를 이용한 게임루프 각각의 interval을 ms단위로 설정한 후 버튼을 클릭하면 10초 동안 매 게임루프에서 카운트를 업하고 10초가 지나면 그동안 카운트된 개수를 10으로 나눠서 1초당 카운트 수를 구하는거죠. 일반적인 FPS 검출 로직과 큰 차이 없다고 생각해요.

그.런.데. 결과는 상당히 흥미롭네요.

1. 인터벌을 0ms(=MaxFramerate) 즉, 최대 값으로 설정했을 때

네, MaxFramerate에 상당하는 거의 오차 범위 내의 값이 나와요. 다만 실버라이트 런타임이 지원하는 최대 Framerate인 60FPS가 되면 다른 것보다 오차가 커지는 것이 약간 거슬리는 정도죠.

2. 인터벌을 15ms(=66FPS) 즉, 일반적인 Windows 시스템 타이머의 최소 해상도로 설정했을 때

이것도 0ms로 했을 때랑 거의 차이가 없어요.

3. 인터벌을 16ms(=62FPS)로 설정했을 때

아주 이상하게도, MaxFramerate를 60으로 설정했을 때 오차가 갑자기 심해지는 현상이 나왔어요. 심지어 50FPS일 때보다도 더 떨어지는 Framerate가 나온거죠.

4. 인터벌을 50ms(=20FPS)로 설정했을 때

별로 높은 FPS도 아닌데 의외로 오차가 심하죠?

이 외에도 수치를 바꿔가면서 테스트 해 보면 잘 이해되지 않는 결과를 얻는데요, 이쯤되니 제 코드도 의심되더군요. 여튼 관심있는 분은 받아서 테스트를 한번 해보세요.

만약 제 실험 설계에 문제가 없다면 다음과 같은 결론을 얻을 수 있어요.

  • 플래시의 onEnterFrame과 같은 게임루프 구현은 가급적 인터벌을 0으로 줘서 실버라이트 런타임에게 맡기고 렌더링을 수행할 것.
  • FPS의 조절은 가급적 MaxFramerate 속성을 설정하여 시스템 전체적으로 조절할 것.

P.S.
자세한 내막에 대한 정보가 있으시면 제보 바랍니다.

저작자 표시 동일 조건 변경 허락
신고
Posted by gongdo
세미나를 준비할 때마다 하는 얘기지만, 이번에도 강행군의 연속이었어요.
이번엔 다른 프로젝트까지 겹쳐서 2주 동안 하루 평균 4시간 정도밖에 못자면서 프로젝트와 세미나 준비를 계속 해왔는데요, 다른 무엇보다 훈스 닷넷 발표자 여러분 모두 수고하셨습니다!

이번 세미나는 연속적인 성격을 가지고 있지만 각 회차마다 전달하고자 하는 내용이 달라서 짧은 세션동안 맡은 부분에 대한 내용을 최대한 우겨 넣을 수 밖에 없었어요.
두~세파트 정도로 나눠서 진행했더라면... 하는 아쉬움이 있네요.

어쨌든 오늘 발표 했던 프리젠테이션 자료를 첨부합니다.
4세션_실버라이트 모션 테크닉.zip

4세션 실버라이트 모션 테크닉


안타깝게도 데모 중간중간에 나오는 블렌드 동영상은 용량 문제로 첨부하지 못했고 혹시 올려 놓을 계정이 있으신 분은 얘기해주시면 올려드리도록 할께요.
첨부한 파일의 DEMO 폴더에 오늘 발표했던 내용의 모든 예제의 소스코드가 들어있으니 참고하세요. 이번엔 너무 급해서 주석처리는 제대로 못했네요. 하지만 이번 발표에서는 코드 쪽은 거의 필요 없고 스토리보드로 만든 애니메이션쪽을 보시면 될 거에요.

마지막으로 소개 했던 물리엔진 데모는 아직 공개하기가 애매해서 조금 더 다듬어 올려볼께요. 생각처럼 쉽게 되진 않는군요^^


세미나에 와주신 모든 분들께 감사드립니다. :)

신고
Posted by gongdo
Storyboard XAML 문법의 난해함

UI 프로그래밍 기법에 있어서 WPF와 Silverlight의 가장 큰 특징은 Storyboard와 Animation 개체를 사용한 애니메이션의 구현이 아닐까 생각해요. 물론 비슷한 개념은 이미 플래쉬나 다른 곳에서도 볼 수 있었지만요.

그런데 이 스토리보드와 애니메이션을 수작업으로 XAML 코딩 한다는 건 만만치가 않습니다. 바로 애니메이션의 대상 개체와 대상 속성을 지정하는 문법의 귀찮음난해함 때문이죠.

간단하게 테스트 해볼까요?
페이지가 로드 되었을 때 다음과 같은 Rectangle 엘리먼트가 오른쪽으로 100픽셀 이동하면서 회전하는 애니메이션을 Expression Blend를 사용하지 않고 코딩해보세요. 단, Orcas를 사용해도 좋습니다.

<Rectangle Width="100" Height="100" Canvas.Left="50" Canvas.Top="100" Fill="Black" />

이걸 고민 없이 해내는 분이 있다면 쓰부로 모실테니 연락주세요!

당연한 얘기지만 전 그렇게 못해요. 그래서 이런 애니메이션을 만들 땐 곧바로 Expression Blend로 프로젝트를 열어보지요.
블렌드로 대충 끄적여 볼까요? 간단한 과정인데 캡쳐하기가 애매해서 동영상으로 캡쳐했어요.


원본은 800 x 600이니까 다운 받아서 보세요.
http://gongdo.tistory.com/attachment/cfile5.uf@25123E3A5878F5810E67EB.wmv


대략 아래와 같은 XAML 코드가 만들어질거에요. (루트 엘리먼트는 생략.)
XAML
<Canvas.Triggers>
    <EventTrigger RoutedEvent="Canvas.Loaded">
        <BeginStoryboard>
            <Storyboard x:Name="Timeline1">
                <DoubleAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetName="rectangle" Storyboard.TargetProperty="(UIElement.RenderTransform).(TransformGroup.Children)[3].(TranslateTransform.X)">
                    <SplineDoubleKeyFrame KeyTime="00:00:01" Value="100"/>
                </DoubleAnimationUsingKeyFrames>
                <DoubleAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetName="rectangle" Storyboard.TargetProperty="(UIElement.RenderTransform).(TransformGroup.Children)[2].(RotateTransform.Angle)">
                    <SplineDoubleKeyFrame KeyTime="00:00:01" Value="360"/>
                </DoubleAnimationUsingKeyFrames>
            </Storyboard>
        </BeginStoryboard>
    </EventTrigger>
</Canvas.Triggers>

<Rectangle Width="100" Height="100" Canvas.Left="50" Canvas.Top="100" Fill="Black" RenderTransformOrigin="0.5,0.5" x:Name="rectangle" >
    <Rectangle.RenderTransform>
        <TransformGroup>
            <ScaleTransform ScaleX="1" ScaleY="1"/>
            <SkewTransform AngleX="0" AngleY="0"/>
            <RotateTransform Angle="0"/>
            <TranslateTransform X="0" Y="0"/>
        </TransformGroup>
    </Rectangle.RenderTransform>
</Rectangle>

와... 이거 코딩은 고사하고 해독하는데도 만만치가 않네요. 단지 몇 픽셀 옮기면서 돌렸을 뿐인데...
특히 이 코드의 백미는 Storyboard.TargetProperty의 복잡함이죠. 죄다 문자열이라서 인텔리센스의 도움을 받을 수 없는데 베이스 클래스의 이름부터 순서대로 쫘악 나열해야 하는데 중간에 배열 번호까지 들어가요. 해당 속성이 컬렉션일 경우엔 추가된 순서까지 맞춰서 코딩해야 한다는 거죠.

이쯤되면 수작업으로 작성하는 건 GG죠.

이런 이유로 실버라잇의 애니메이션은 그냥 블렌드를 사용하여 작성하는게 정신 건강에 훨씬 좋아요. 블렌드에서 대충 원하는 형태의 애니메이션을 만들고 세부적인 값을 수정하는 거죠.

현재 스토리보드의 문제점

그나마도 디자인타임에 애니메이션이 고정되는 건 블렌드와 약간의 수정을 통해 뭐라도 만들 수 있어요. 하지만 동적으로 순간순간 변화하는 변수를 적용해야 하는 애니메이션이 있다면? 그럼 더 머리 아파지죠.

여기에서 XAML과 코드-비하인드를 연계하여 코딩하는데 익숙하신 분이라면 XAML의 Canvas나 TransformGroup 엘리먼트처럼 하위 엘리먼트를 포함할 수 있는 경우 코드-비하인드에서는 object.Children 이라는 컬렉션 속성을 통해 하위 엘리먼트에 접근하거나 추가/삭제 할 수 있다는 걸 아실거에요. 예를 들어 위에서 작성한 XAML을 개체 트리로 나타내 보자면 다음과 같습니다.


이것을 다시 의사 코드로 작성해보면 아마도 아래와 같겠죠
C# (의사 코드)
Canvas canvas = new Canvas();    // 새 캔버스 생성

BeginStoryboard bs = new BeginStoryboard();    // 새 스토리보드 시작기 생성

Storyboard sb = new Storyboard();    // 새 스토리보드 생성

DoubleAnimationUsingKeyFrames dak = new DoubleAnimationUsingKeyFrames();    // 더블 애니메이션 생성
dak.KeyFrames.Add( new SplineDoubleKeyFrame() );    // 애니메이션에 키프레임 추가

sb.Children.Add( dak );    // 스토리보드에 생성한 애니메이션 추가

dak = new DoubleAnimationUsingKeyFrames();    // 다른 애니메이션 새로 추가
dak.KeyFrames.Add( new SplineDoubleKeyFrames() );

sb.Children.Add( dak );

bs.Storyboard = sb;    // 스토리보드를 설정

Rectangle rect = new Rectangle();    // 새 사각형 생성

TransformGroup tg = new TransformGroup();    // 새 변형 그룹 생성

tg.Children.Add( new ScaleTransform() );        // 변형 그룹에 변형 규칙 추가
tg.Children.Add( new SkewTransform() );
tg.Children.Add( new RotateTransform() );
tg.Children.Add( new TranslateTransform() );

rect.RenderTransform = tg;    // 사각형의 변형 속성을 설정

canvas.Children.Add( tg );    // 사각형을 캔버스에 추가

canvas.Triggers.Add( bs );    // 스토리보드 시작기를 캔버스의 트리거에 추가

꽤나 복잡해 보이지만 위의 개체 트리 구조를 보면 별로 어려울 건 없을 거에요. XAML에서 하위 요소는 코드-비하인드에서 일반적으로 같은 이름의 컬렉션으로 구현된다는 거죠.
이렇게라도 동작해주면 얼마나 좋겠습니까...만,
안됩니다.

TransformGroup.Children이나 Canvas.Children 또는 Canvas.Triggers와 같은 Collection 개체는 일반적으로 하위 개체를 생성하여 추가, 삭제, 접근할 수 있게 되어 있어요.

하지만 이상하게도 Storyboard의 Children 속성은 분명히 TimelineGroup이라는 이름의 클래스임에도 불구하고 실제로 들어가보면 단지 Timeline을 가리킬 뿐이에요. 다시 말해서 컬렉션이 아니기 때문에 하위 요소를 하나밖에 추가할 수 없다는 얘기죠.

예제에서 우리는 [오른쪽으로 이동] 이라는 애니메이션과 [360도 회전]이란 애니메이션을 동시에 처리하였죠? 그런데 스토리보드의 Children이 단일 요소만 가리키게 되어 있으니 이건 뭐 방법이 없는 거에요.

이 부분은 아마도 1.1 Alpha라는 테스트 중인 버전이기 때문에 그렇지 않을까... 정식판이 나오면 스토리보드의 Children이 Animation의 컬렉션으로 변경되지 않을까... 하는 기대를 할 수밖에 없네요.

XamlReader 클래스를 활용한 동적 스토리보드 작성

그렇다고 코드에서 스토리보드를 동적으로 생성하고 애니메이션을 추가하는게 불가능한 건 아니에요.
다만 효율이 조금 떨어질 뿐이죠.

실버라잇은 XamlReader라는 매우 리버럴한 클래스를 제공하는데요, XamlReader.Load 메서드를 사용하면 파라미터로 전달한 XAML 코드를 동적으로 파싱하여 개체로 반환해주죠.

예를 들어 새 사각형 개체를 XamlReader를 이용하여 동적으로 추가한다면...

C#
string strRect = @"<Rectangle Width='100' Height='100' Canvas.Left='100' Canvas.Top='100' Fill='Red' />";
Rectangle rect = (Rectangle)XamlReader.Load(strRect);
this.Children.Add(rect);

심플하죠? 그냥 동적으로 추가할 XAML 코드를 문자열로 저장하고 XamlRead.Load 메서드의 파라미터로 전달하고 원하는 타입으로 캐스팅만 하면 돼요.

마찬가지로 스토리보드와 일련의 요소들을 XAML 코드로 작성하여 XamlReader로 동적으로 생성할 수 있다는 거죠.
처음 Blend로 작성했던 예제를 조금 변형해서 이번엔 텍스트1을 클릭하면 오른쪽으로 100픽셀, 텍스트2를 클릭하면 왼쪽으로 100픽셀을 회전하며 이동하는 코드를 작성해보죠.

먼저 텍스트1, 2과 결과 확인을 위한 텍스트3을 추가하고 기존에 작성되었던 애니메이션은 제거하며 사각형은 그대로 놔둡니다. 역시 루트 엘리먼트인 Canvas는 생략했으니 주의...
XAML
<TextBlock x:Name="text1" Text="You spin me RIGHT round round round..." Canvas.Top="0" />
<TextBlock x:Name="text2" Text="You spin me LEFT round round round..." Canvas.Top="20" />
<TextBlock x:Name="text3" Text="O_O" Canvas.Top="40" />


<Rectangle Width="100" Height="100" Canvas.Left="50" Canvas.Top="100" Fill="Black" RenderTransformOrigin="0.5,0.5" x:Name="rectangle" >
    <Rectangle.RenderTransform>
        <TransformGroup>
            <ScaleTransform ScaleX="1" ScaleY="1"/>
            <SkewTransform AngleX="0" AngleY="0"/>
            <RotateTransform Angle="0"/>
            <TranslateTransform X="0" Y="0"/>
        </TransformGroup>
    </Rectangle.RenderTransform>
</Rectangle>

오른쪽/왼쪽으로 움직이는 스토리보드가 하나 있어야겠죠? 클래스의 멤버 변수로 스토리보드를 하나 선언합니다.
C#
Storyboard _story = new Storyboard();

그리고 페이지의 로드 이벤트 핸들러에 텍스트1과 2가 눌렸을 때 처리할 이벤트 핸들러를 추가합니다. 이벤트 핸들러는 아래에서 채워 넣을 거에요.
C#
// 텍스트 클릭 이벤트 핸들러 추가
text1.MouseLeftButtonDown += new MouseEventHandler(text1_MouseLeftButtonDown);
text2.MouseLeftButtonDown += new MouseEventHandler(text2_MouseLeftButtonDown);

이제 이벤트 핸들러에 각각 스토리보드를 로드하는 코드를 만들어야겠죠? 이 작업은 앞서 얘기했듯이 수작업으로 머리 싸매가면서 할 필요 없어요. 그냥 Expression Blend로 빈 프로젝트를 하나 만들고 애니메이션 타임라인을 생성하되 새 타임라인을 추가할 때 리소스로 만들기(Create as a Resource)를 선택하세요.


그리곤 원하는 애니메이션을 대충 만들면 <Canvas.Resources> 태그 내에 맨 처음에 Blend로 만들었던 Storyboard 코드가 만들어질 거에요. 여기에서 <Storyboard>의 x:Name이 Timeline1로 지정되어 있는데 이건 코드에서 동적으로 로드하기 때문에 필요 없으니 삭제하세요. 마지막으로 각 태그의 어트리뷰트 설정을 위해 겹따옴표가 있는데 이건 홑따옴표로 교체하시고 코드-비하인드에 문자열로 붙여넣으시면 다음과 같은 코드가 만들어지죠.
C#
void text1_MouseLeftButtonDown(object sender, MouseEventArgs e)
{
string strStory =
    @"<Storyboard>
        <DoubleAnimationUsingKeyFrames BeginTime='00:00:00' Storyboard.TargetName='rectangle' Storyboard.TargetProperty='(UIElement.RenderTransform).(TransformGroup.Children)[3].(TranslateTransform.X)'>
            <SplineDoubleKeyFrame KeyTime='00:00:01' Value='100'/>
        </DoubleAnimationUsingKeyFrames>
        <DoubleAnimationUsingKeyFrames BeginTime='00:00:00' Storyboard.TargetName='rectangle' Storyboard.TargetProperty='(UIElement.RenderTransform).(TransformGroup.Children)[2].(RotateTransform.Angle)'>
            <SplineDoubleKeyFrame KeyTime='00:00:01' Value='360'/>
        </DoubleAnimationUsingKeyFrames>
    </Storyboard>";

    _story = (Storyboard)XamlReader.Load(strStory);
    _story.Completed += new EventHandler(_story_Completed);
    this.Resources.Add(_story);

    _story.Begin();
    text3.Text = "Spin started.";
}

void
_story_Completed(object sender, EventArgs e)

{
    text3.Text = "Spin completed.";
}

text2를 눌렀을 때의 이벤트 핸들러 코드는 생략했어요. (갈수록 날로 먹으려는 이 수작!)
뭐 text1의 핸들러와 똑같고 두 애니메이션의 Value값을 '0'으로만 바꾸면 되니까 직접 해보세요. -ㅇ-

여기서 주의할 점은 코드로 제어하는 스토리보드는 반드시 루트 엘리먼트의 리소스로 등록이 되어야 한다는 것과 XamlReader로 동적으로 로드하는 XAML 엘리먼트는 x:Name이 설정되면 안된다는 사실. 잊지 마세요.

작성된 프로젝트는 다음에 첨부했으니 코딩이 귀찮으신 분들은 다운 받아보세요. :)
StoryboardTest.zip

스토리보드 테스트 프로젝트



동작 데모는 다음과 같습니다.

http://gongdo.tistory.com/attachment/cfile27.uf@253353395878F57F1002F7.wmv

여전히 남아있는 불합리성

여기까지 더운데 땀 삐질삐질 흘려가며 작성하셨다면 지금쯤 꽤나 열이 받으셨을 거에요.

"블렌드를 써서 만든 코드를 복사해서 도로 붙여 넣고 따옴표 바꿔주고 문자열로 만들고 코드 몇 줄 더 추가하고... 이럴 바에야 스토리보드를 각각에 상황에 맞게 더 만들고 말겠다!"

네, 정답입니다. 위의 예제 같은 경우는 구태여 코드에서 동적으로 만들 필요성 따윈 전혀 없어요.
단순한 애니메이션이라면 그냥 Blend를 써서 만드는게 훨씬 빠르고 합리적이죠.

하지만 이런 경우를 생각해보죠. 만약 애니메이션의 이동 범위가 런타임에 수시로 바뀐다면? 심지어 이동 방향 조차 변경될 수 있다면?
또는 마우스 커서가 이동하면 마우스 커서에서 시작되는 애니메이션을 만들고 싶다면?

이럴 때 위와 같이 문자열로 XAML 코드를 옮기되 동적으로 수정하고 싶은 부분을 {0}, {1}, {2} ... 이렇게 치환해놓고 String.Format 메서드로 포매팅하면 되겠죠. 예를 들어 위의 예제에서 진행시간, 이동 범위를 동적으로 변경한다면...
C#
string strStory =
    @"<Storyboard>
        <DoubleAnimationUsingKeyFrames BeginTime='00:00:00' Storyboard.TargetName='rectangle' Storyboard.TargetProperty='(UIElement.RenderTransform).(TransformGroup.Children)[3].(TranslateTransform.X)'>
            <SplineDoubleKeyFrame KeyTime='{0}' Value='{1}'/>
        </DoubleAnimationUsingKeyFrames>
        <DoubleAnimationUsingKeyFrames BeginTime='00:00:00' Storyboard.TargetName='rectangle' Storyboard.TargetProperty='(UIElement.RenderTransform).(TransformGroup.Children)[2].(RotateTransform.Angle)'>
            <SplineDoubleKeyFrame KeyTime='{0}' Value='360'/>
        </DoubleAnimationUsingKeyFrames>
    </Storyboard>";

    _story = (Storyboard)XamlReader.Load( string.Format( strStory, new object[] { "00:00:05", "200" } ) );

이렇게 문자열의 포매팅을 활용할 수 있겠죠.

휴...
스토리보드, 애니메이션, 타임라인은 다른 개체들이 상당히 직관적으로 구성되어 있는데 비해 굉장히 난해한 편인 것 같아요. 그냥 개체나 멤버의 이름만 보고 직관적인 코딩이 어렵다는 거죠. 코드만 가지고 적절하게 스토리보드를 만들지 못하고 상당히 지저분한 방법을 동원하게 되었는데요, 다시 얘기하지만 이런 점은 정식판이 나오면서 개선되었으면 하는 바램이에요.

신고
Posted by gongdo


티스토리 툴바