BackacheEngineerの技術的な備忘録

技術系でいろいろ書けたらなーと

XAML: デザインの座標変換(基本のみ)

XAMLの座標変換の基本についてまとめる。まとめる内容は以下のうちで、平行性を保った変換(アフィン変換という)のみとする。 アフィン変換ではない、図形の平行性を維持しない(ぐちゃぐちゃに変形できる)ような座標変換(テーパー変換という)は扱わない。

平行移動

図形を平行移動させる。使いどころは、sin波やcos波など波形を扱う場合で、位相差による差分等を比較したいときや、どのような変化をするのかアニメーションで見たいとき。

使うのは「RenderTransform」の「TranslateTransform」。 「X」と「Y」というプロパティを持っており、値を設定して正の方向、負の方向に移動させられる。

今回見せる例はもっと簡単な影付き文字など。 X方向、Y方向に微妙にずらして浮き上がっているように見せたり、へこんでいるように見せたり、影がついているように見える。

f:id:BackacheEngineer:20210424213244p:plain
平行移動を使って影を付けた文字

xamlコード(Window部分は省略)

    <Window.Resources>
        <Style TargetType="TextBlock">
            <Setter Property="FontFamily" Value="Times New Roman" />
            <Setter Property="FontSize" Value="102" />
            <Setter Property="HorizontalAlignment" Value="Center" />
            <Setter Property="VerticalAlignment" Value="Center" />
        </Style>
    </Window.Resources>

    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="*" />
            <RowDefinition Height="*" />
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>

        <TextBlock Text="EMBOSS" Grid.Row="0"/>
        <TextBlock Text="EMBOSS" Grid.Row="0"
                   Foreground="White">
            <TextBlock.RenderTransform>
                <TranslateTransform X="-2" Y="-2"/>
            </TextBlock.RenderTransform>
        </TextBlock>

        <TextBlock Text="ENGRAVE" Grid.Row="1"/>
        <TextBlock Text="ENGRAVE" Grid.Row="1"
                   Foreground="White">
            <TextBlock.RenderTransform>
                <TranslateTransform X="2" Y="2"/>
            </TextBlock.RenderTransform>
        </TextBlock>

        <TextBlock Text="Drop Shadow" Grid.Row="2"
                   Foreground="Gray">
            <TextBlock.RenderTransform>
                <TranslateTransform X="5" Y="5"/>
            </TextBlock.RenderTransform>
        </TextBlock>
        <TextBlock Text="Drop Shadow" Grid.Row="2"/>

    </Grid>

拡大・縮小

図形を拡大・縮小する。これが最も使うかもしれないやつ。画像を拡大・縮小もするだろうし、平行移動でも扱った波形も拡大したいときがあるだろう。

使うのは「RenderTransform」の「ScaleTransform」。 「ScaleX」と「ScaleY」というプロパティを持っており、X方向、Y方向別々に拡大・縮小できる。

拡大・縮小はイベントやアニメーションとの組み合わせになるため、参考画像はなし。 XAMLコードだけ載せる。

先に書いてしまうと、座標変換はもうだいたいこの形になる。

<TextBlock Text="Sample">
    <TextBlock.RenderTransform>
        <ScaleTransform ScaleX="2"/>
    </TextBlock.RenderTransform>
</TextBlock>

傾斜

図形を傾かせる。そのまんまである。使いどきは、任意に図形を傾かせるという機能が必要なツール。絵を描くツールがその代表と考えられる。

使うのは「RenderTransform」の「SkewTransform」。 「AngleX」と「AngleY」というプロパティを持ち、X方向、Y方向に傾けられる。

サンプルは影を傾かせた文字。平行移動も混じっているが、結局この複数の変換を実施するのも後々まとめる。

f:id:BackacheEngineer:20210424220143p:plain
傾きのサンプル

xamlコードはこんな感じ。RenderTransformOriginは軸中心をいじくるもの。次の回転でまとめる。

    <Window.Resources>
        <Style TargetType="TextBlock">
            <Setter Property="Text" Value="quirky" />
            <Setter Property="FontFamily" Value="Times New Roman" />
            <Setter Property="FontSize" Value="100" />
            <Setter Property="HorizontalAlignment" Value="Center" />
            <Setter Property="VerticalAlignment" Value="Center" />
        </Style>
    </Window.Resources>

    <Grid>
        <TextBlock Foreground="Gray"
                   RenderTransformOrigin="0 1">
            <TextBlock.RenderTransform>
                <!-- compositetransform なんてあるらしい。複数変換をまとめたもの。 -->
                <TransformGroup>
                    <ScaleTransform ScaleY="1.5"/>
                    <SkewTransform AngleX="-60"/>
                </TransformGroup>
            </TextBlock.RenderTransform>
        </TextBlock>

        <TextBlock/>
    </Grid>

回転

図形を回転させる。言ってしまえば、座標に回転行列をかけるもの。これまで説明してきたやつも、すべて行列演算にまとめられる(次章でまとめる)。 実際に使うときは、傾斜と同様に絵を描くツールだろうか。ほかにもありそうだが、自身の経験が偏っているのか思いつかない。

使うのは「RenderTransform」の「RotateTransform」。 「Angle」プロパティを持っており、角度を調整する。

サンプル画像とるのも面倒だったので(おい)xamlコードだけ載せる。

<TextBlock Text="Rotate Text">
    <TextBlock.RenderTransform>
        <RotateTransform Angle="60"/>
    </TextBlock.RenderTransform>
</TextBlock>

実は上記のコードだと回転軸が中心じゃなかったりする。これは、既定の回転軸が(0, 0)であり、この座標は図形の左上を指しているからである。

これを変更するには「RenderTransformOrigin」で回転軸を変更する必要がある。回転軸を中心にずらしたxamlコードは下記のとおり。

<TextBlock Text="Rotate Text"
                   RenderTransformOrigin="0.5 0.5">
    <TextBlock.RenderTransform>
        <RotateTransform Angle="60"/>
    </TextBlock.RenderTransform>
</TextBlock>

座標変換のまとめ

座標変換の基本をまとめた。最後の回転で少し触れたが、これらすべての変換は一つの行列演算で表せる。 元の座標を  (x_0,  y_0) とし、変換後の座標を  (x_1,  y_1) とする。

 \displaystyle
(x_{1}, y_{1},  1) = (x_{0}, y_{0},  1)
\begin{pmatrix}
X_{11} & X_{12} & 0 \\\ 
Y_{21} & Y_{22} & 0 \\\
OffsetX & OffSetY & 1
\end{pmatrix}

これを解くと下記のとおり。

 \displaystyle
x_{1} = X_{11}*x_{0} + Y_{21}*y_{0} + OffsetX\\\
y_{1} = X_{12}*x_{0} + Y_{22}*y_{0} + OffsetY

 X_{11} Y_{21} は見てのとおり係数である。 拡大、縮小、傾斜、回転すべてこれである。 回転になると  X_{11} = cos(A) といった回転角となる(Aは角度、ラジアン)。 そして、 OffsetX  OffsetY が平行移動である。

 Offset に対しては係数がない。というか、ないのが普通になるはずだ。 主な理由は「一般的に考えて、回転や拡大の係数が平行移動に係ってほしくない」というもの。

なんでこんな行列演算をまとめたかというと、XAMLでこれをサクッと書けるため。 「RenderTransform」さんに上記で示した係数を渡してやると勝手にやってくれるのだ・・・。

「RenderTransform = " X_{11} \  X_{12} \  Y_{21} \  Y_{22} \  OffsetX \  OffsetY "」

この形式で書いてやればいい(各値は空白区切り)。

f:id:BackacheEngineer:20210424230233p:plain
行列演算を自動でやってくれるRenderTransform

xamlコードは下記のとおり。

    <Window.Resources>
        <Style TargetType="TextBlock">
            <Setter Property="FontSize" Value="24" />
            <Setter Property="RenderTransformOrigin" Value="0 0.5" />
        </Style>
    </Window.Resources>
    
    <Grid>
        <Canvas HorizontalAlignment="Center" VerticalAlignment="Center">
            <TextBlock Text="   RenderTransform='1 0 0 1 0 0'"
                       RenderTransform="1 0 0 1 0 0"/>
            <TextBlock Text="   RenderTransform='.7 .7 -.7 .7 0 0'"
                       RenderTransform=".7 .7 -.7 .7 0 0"/>
            <TextBlock Text="   RenderTransform='0 1 -1 0 0 0'"
                       RenderTransform="0 1 -1 0 0 0"/>
            <TextBlock Text="   RenderTransform='-.7 .7 -.7 -.7 0 0'"
                       RenderTransform="-.7 .7 -.7 -.7 0 0"/>
            <TextBlock Text="   RenderTransform='-1 0 0 -1 0 0'"
                       RenderTransform="-1 0 0 -1 0 0"/>
            <TextBlock Text="   RenderTransform='-.7 -.7 .7 -.7 0 0'"
                       RenderTransform="-.7 -.7 .7 -.7 0 0"/>
            <TextBlock Text="   RenderTransform='0 -1 1 0 0 0'"
                       RenderTransform="0 -1 1 0 0 0"/>
            <TextBlock Text="   RenderTransform='.7 -.7 .7 .7 0 0'"
                       RenderTransform=".7 -.7 .7 .7 0 0"/>
        </Canvas>    
    </Grid>

座標変換の組み合わせとアニメーション

最後に、これら座標変換を組み合わせた「TransformGroup」とアニメーションを使った例をまとめる。

一つの図形に複数の変換を実施したい場合は「TransformGroup」でまとめてやるだけだ。 そして、各変換値を「DoubleAnimation」でごりごり動かす。

ちょっとレートが低くてカクカクだが、こんな感じだ。

f:id:BackacheEngineer:20210424231521g:plain
プロペラ回転のアニメーション

xamlコードは以下のとおり。

    <Grid>
        <Polygon Points="40 0, 60 0, 53 47,
                        100 40, 100 60, 53 53,
                        60 100, 40 100, 47 53,
                        0 60, 0 40, 47 47"
                 Fill="SteelBlue"
                 HorizontalAlignment="Center"
                 VerticalAlignment="Center"
                 RenderTransformOrigin="0.5 0.5">
            <Polygon.RenderTransform>
                <TransformGroup>
                    <!-- ここの回転軸はプロペラの中心(画面中央) -->
                    <RotateTransform x:Name="rotate1"/>
                    <!-- ここでプロペラが移動する。なお、プロペラは移動するが、回転軸は変わらず画面中央 -->
                    <TranslateTransform X="300"/>
                    <!-- 回転軸は初期位置(画面中央)なので、この回転は大きな円軌道となる。 -->
                    <RotateTransform x:Name="rotate2"/>
                </TransformGroup>
            </Polygon.RenderTransform>
        </Polygon>
    </Grid>

    <Window.Triggers>
        <EventTrigger RoutedEvent="MouseEnter">
            <BeginStoryboard>
                <Storyboard>
                    <DoubleAnimation Storyboard.TargetName="rotate1"
                                     Storyboard.TargetProperty="Angle"
                                     From="0" To="360" Duration="0:0:0.5"
                                     RepeatBehavior="Forever"/>
                    <DoubleAnimation Storyboard.TargetName="rotate2"
                                     Storyboard.TargetProperty="Angle"
                                     From="0" To="360" Duration="0:0:6"
                                     RepeatBehavior="Forever"/>
                </Storyboard>
            </BeginStoryboard>
        </EventTrigger>
    </Window.Triggers>