コルネの進捗や備忘録が記されたなにか

進捗や成果物や備忘録てきななにかを雑に更新していきます。

【Power Apps】あみだくじを作成してみよう!-その3-


スポンサードリンク

はじめに

この記事は、

の続きとなります。
まだ上記をご覧になられていない場合は、先に確認することをお勧めします。

これまでの記事では、あみだくじの梯子部分を作成しました。
今回は参加者を表示させ、その参加者たちがあみだくじに沿って結果まで動く。というアニメーションの作成方法を解説したいと思います。

あみだくじに沿って参加者を動かすアニメーションを作成する

各参加者の移動ルートを計算する

animateMotion というSVG の要素を利用することで、指定の要素をモーションパスに沿って移動させるアニメーションを作成することが可能です。

これを利用して、参加者をあみだくじに沿って動かすアニメーションを作成したいと思います。

そのためにまず、各参加者が辿るあみだくじのルートを計算したいと思います。

このルートの計算は、あみだくじ画面が表示されたときに行うようにしたいと思います。

Amidakuji.OnVisible

Clear(route);
Clear(tmpCol);
ForAll(
    Sequence(ParticipantDropdown.Selected.Value, 1) As selectNum,
    Collect(tmpCol, {val:selectNum.Value, now:selectNum.Value});
    ForAll(
        Sequence(CountRows(amidakuji)) As countNum,
        If(
            LookUp(amidakuji, cnt = countNum.Value && num = Last(tmpCol).now, isline),
            Collect(tmpCol, {val:selectNum.Value, now:Last(tmpCol).now + 1});
            Collect(
                route,
                {num:selectNum.Value, cnt:countNum.Value, change:Last(tmpCol).now}
            ),
            LookUp(amidakuji, cnt = countNum.Value && num = Last(tmpCol).now - 1, isline),
            Collect(tmpCol, {val:selectNum.Value, now:Last(tmpCol).now - 1});
            Collect(
                route,
                {num:selectNum.Value, cnt:countNum.Value, change:Last(tmpCol).now}
            );
        );
    );
);

route というコレクションに各参加者が辿る経路を保存しています。
tmpCol というコレクションは、一時保存用のコレクションです。
このコレクションは最後のレコードを確認すれば、現在みているのが何人目の参加者用の経路で、現在何番目の縦線をみているのかを判断できるようになっています。

それぞれのコレクションには以下のような値が入ることを想定しています。

route

アイテム名 役割
num x人目の参加者
cnt y番目の箇所
change 変更後の縦線番号

tmpCol

アイテム名 役割
val x人目の参加者
now 現在みている縦線番号

なぜ tmpCol を使用しているか?というと、ForAll 関数 では UpdateContext 関数Set 関数 を用いることができないからです。

ForAll 関数の「アクションの実行」セクション を確認してみましょう。

順序の依存関係を避けるように注意してください。 したがって、UpdateContext、Clear、および ClearCollect 関数は、効果の影響を受けやすい変数を保持するために簡単に使用できるため、 ForAll 関数で使用することはできません。 Collect を使用することはできますが、レコードが追加される順序は定義されていません。

Collect、Remove、および Update を含むデータ ソースを変更する複数の関数は、変更されたデータ ソースを戻り値として返します。 これらの戻り値は大きく、ForAll テーブルのすべてのレコードに対して返された場合、かなりのリソースを使用する可能性があります。 また、ForAll が並行して動作でき、結果の取得からこれらの関数の副作用を切り離す場合があるので、これらの戻り値が予想どおりのものではないこともあります。 幸いにも、ForAll からの戻り値が実際には使用されない場合 (データ変更関数の場合によく見られます)、戻り値は作成されないため、リソースまたは順序付けに関する懸念事項はありません。 ただし、ForAll の結果およびデータ ソースを返す関数のいずれかを使用している場合、結果の構造方法について十分検討し、最初は小さいデータ セットで試してください。

上記のようにデータソースを更新するような関数は利用できないと明記されています。(2020年11月現在)

そこで今回 ForAll 関数 内で疑似的に変数を更新する方法として tmpCol を利用しているというわけです。

今回のルート計算では、ForAll 関数入れ子で利用しています。
なので、現在処理されているレコードの名前解決を行うためにAs 演算子 を利用しています。

まず最初のループ処理ですが、参加者の人数分処理を行います。

その際、 tmpCol にこれから計算を行う参加者の番号を設定します。
この tmpCol に設定された最新の値を取得するには、 Last 関数 を利用します。

子要素のForAll 関数では、 amidakuji コレクションのレコード数分、つまり'参加人数 × 15' 回処理を行います。
これにより、各参加者に対して、一要素ずつ横線が引かれているか?引かれていないか?を判断することができます。

今確認を行っている縦線の y 位置に対して、右方向に横線が引かれているか?は

LookUp(amidakuji, cnt = countNum.Value && num = Last(tmpCol).now, isline)

で確認を行っています。

左方向に横線が引かれているか?は

LookUp(amidakuji, cnt = countNum.Value && num = Last(tmpCol).now - 1, isline)

で判断しています。

まず横線が引かれているか否かをみるためには、 amidakuji コレクションを確認すればいいですね。
この amidakuji コレクションで線が引かれているかは isline を確認すればわかります。

なので、LookUp 関数 を用いて対象箇所(対象レコード)に線が引かれているかをそれぞれ判定しています。

さて、レコードを絞る条件ですが、まず num = Last(tmpCol).now により、現在みている縦線要素を絞っています。
続いて横線要素は cnt = countNum.Value で絞っています。
ただしこれで絞れるのは"右方向に横線が引かれているか?"です。
あみだくじでは、左方向に線が引かれているか?もみなくてはいけません。
この問題を解決する方法として、"ひとつ前の縦線から右方向に横線が引かれているか?"を判断することにより、左方向の横線の有無を判断しています。

ひとつ前の縦線をみたいですから、 num = Last(tmpCol).now - 1 で絞り込めますね。

あとは条件にあったときに、以下でコレクションに値をセットするだけですね。

Collect(tmpCol, {val:selectNum.Value, now:Last(tmpCol).now + 1});
Collect(
    route,
    {num:selectNum.Value, cnt:countNum.Value, change:Last(tmpCol).now}
)

これにより、各参加者が辿る横線をを把握することができました。

余談ですが、今回 amidakuji コレクションのレコード数分だけループしましたが、Filter 関数 を用いて、無駄なループ回数を減らしたほうが処理速度の面から考えるといいかもですね。
ただ、Filter 処理を行うときも内部ではループ処理を行っていると思うので、どちらが本当に性能がいいのか?というのはちょっとわかりません・・・
Power Apps で性能を検証するツールとかあるのだろうか?というよりそこまでしたい需要あるのですかね?

参加者を表示する

参加者の名前は最初の画面で入力された情報を用います。
参加者の名前は ParticipantGallery のテキスト入力に記載されていますので、そこから取得します。

参加者の名前の外観ですが、黒で塗りつぶした四角形に白文字で参加者の名前を表示するようにしたいと思います。

f:id:koruneko:20201127003719p:plain

このような感じにします。

Image.Image

Concat(
    ForAll(
        Sequence(ParticipantDropdown.Selected.Value) As roopcnt,
         "<g>
             <rect x='" & Amidakuji.Width * roopcnt.Value / (ParticipantDropdown.Selected.Value + 1) - 50 & "' y='85' width='100' height='30' fill='black'/>
             <text x='" & Amidakuji.Width * roopcnt.Value / (ParticipantDropdown.Selected.Value + 1) - 
                         Len(Last(FirstN(ParticipantGallery.AllItems, roopcnt.Value)).ParticipantTextInput.Text) * 10 & "' 
                   y='106' 
                   font-size='20' 
                   fill='white' 
                   font-weight='bold'
                   > " &
                 Last(FirstN(ParticipantGallery.AllItems, roopcnt.Value)).ParticipantTextInput.Text & "
             </text>
         </g>"
    ),
    Value
)

まずループの回数ですが、参加人数分だけループを行いたいので、参加人数を選択するドロップダウンで選択された数だけループさせます。 ParticipantDropdown.Selected.Value

<g>要素 他のSVG 要素をグループ化するために用いています。
こちらの要素がなくとも問題なく表示はされます。
しかし、参加者情報はあみだくじに沿って動くようにしたいです。
その際、<rect>要素<text>要素 両方に同じ設定を記載するのは無駄ですね。
なので、これらはグループ化してあげて纏めてアニメーションを設定してあげましょう!

<g>要素 の中で記載されている<rect>要素 は黒塗りの四角を、<text>要素 では参加者の名前をそれぞれ表示するよう記載しています。

文字の表示は Last(FirstN(ParticipantGallery.AllItems, roopcnt.Value)).ParticipantTextInput.Text で行っています。
これにより、最初の画面で記載された順に参加者の名前が表示されますね。

参加者が辿るルートを表示する

参加者があみだくじを辿るアニメーションを作成するには、冒頭でanimateMotion というSVG の要素を利用すると説明しましたが、こいつを利用するためには、アニメーションの軌跡が作成されている必要があります。
ですので、「各参加者の移動ルートを計算する」で得られた情報をもとに各参加者の軌跡を作成したいと思います。

なおこの線は見えなくてよいため透明な線で作成します。

Image.Image

Concat(
        ForAll(
            Sequence(ParticipantDropdown.Selected.Value) As roopcnt,
            "<path d='M " & Amidakuji.Width * roopcnt.Value / (ParticipantDropdown.Selected.Value + 1) & " 100 " &
             "L " & Amidakuji.Width * roopcnt.Value / (ParticipantDropdown.Selected.Value + 1) & " " &
              First(Filter(route, num = roopcnt.Value)).cnt * 30 + 100 &
            Concat(
                ForAll(
                    Sequence(CountRows(Filter(route, num = roopcnt.Value))) As routeNum,
                    " L " &  Amidakuji.Width * Last(FirstN(Filter(route, num = roopcnt.Value), routeNum.Value)).change / (ParticipantDropdown.Selected.Value + 1) & " " &
                      Last(FirstN(Filter(route, num = roopcnt.Value), routeNum.Value)).cnt * 30 + 100 &
                       "L " & Amidakuji.Width * Last(FirstN(Filter(route, num = roopcnt.Value), routeNum.Value)).change / (ParticipantDropdown.Selected.Value + 1) & " " &
                        Last(FirstN(Filter(route, num = roopcnt.Value), routeNum.Value + 1)).cnt * 30 + 100
                ),
                Value
            ) & 
            " L " & Amidakuji.Width * Last(Filter(route, num = roopcnt.Value)).change / (ParticipantDropdown.Selected.Value + 1) & " 650'" &
             " stroke='none' fill='transparent' stroke-width='1'  id='theMotionPath " & roopcnt.Value &"' />"
        ),
        Value
)

親要素のループでは、1人目の参加者、2人目の参加者・・・といった順でループするようにしています。

まずはあみだくじの最初の横線が引かれるヶ所まで縦線を引きます。

Sequence(ParticipantDropdown.Selected.Value) As roopcnt,
"<path d='M " & Amidakuji.Width * roopcnt.Value / (ParticipantDropdown.Selected.Value + 1) & " 100 " &
 "L " & Amidakuji.Width * roopcnt.Value / (ParticipantDropdown.Selected.Value + 1) & " " &
  First(Filter(route, num = roopcnt.Value)).cnt * 30 + 100

子要素のループでは、 計算したルートが格納されている route コレクションの、現在ルートを作成しているX人目の参加者分だけループを行うようにしています。

このループでは直線コマンド L が2つ存在します。

まず1つめの L ですが、これは横線を引いています。

" L " &  Amidakuji.Width * Last(FirstN(Filter(route, num = roopcnt.Value), routeNum.Value)).change / (ParticipantDropdown.Selected.Value + 1) & " " &
  Last(FirstN(Filter(route, num = roopcnt.Value), routeNum.Value)).cnt * 30 + 100

2つめの L ですが、これは縦線を引いています。

"L " & Amidakuji.Width * Last(FirstN(Filter(route, num = roopcnt.Value), routeNum.Value)).change / (ParticipantDropdown.Selected.Value + 1) & " " &
 Last(FirstN(Filter(route, num = roopcnt.Value), routeNum.Value + 1)).cnt * 30 + 100

これを繰り返すことで、格子部分の軌跡を作成できますね。

最後に親要素の入れ子の最後であみだくじの最後まで縦線を引きます。

" L " & Amidakuji.Width * Last(Filter(route, num = roopcnt.Value)).change / (ParticipantDropdown.Selected.Value + 1) & " 650'"

色は無しにしたいので、 stroke='none' を、また、これらをアニメーションの軌跡に設定したいので id='theMotionPath " & roopcnt.Value と設定しましょう。
この id で、アニメーションの軌跡を判断します。

参加者をあみだくじに沿って動かす

参加者の名前をあみだくじに沿って動かすようにします。
先ほど作成した参加者の名前を表示するための記載を以下のように置き換えてください。

Image.Image

Concat(
    ForAll(
        Sequence(ParticipantDropdown.Selected.Value) As roopcnt,
        If(
            isStart,
            "<g>
                <rect x='-50' y='-15' width='100' height='30' fill='black'/>
                <text x='-" & Len(Last(FirstN(ParticipantGallery.AllItems, roopcnt.Value)).ParticipantTextInput.Text) * 10 & "' 
                      y='6' 
                      font-size='20' 
                      fill='white' 
                      font-weight='bold'
                      > " &
                    Last(FirstN(ParticipantGallery.AllItems, roopcnt.Value)).ParticipantTextInput.Text & "
                </text>
                <animateMotion dur='7s' repeatCount='1' fill='freeze' >
                    <mpath xlink:href='#theMotionPath " & roopcnt.Value &"' />
                </animateMotion>
            </g>",
            "<g>
                <rect x='" & Amidakuji.Width * roopcnt.Value / (ParticipantDropdown.Selected.Value + 1) - 50 & "' y='85' width='100' height='30' fill='black'/>
                <text x='" & Amidakuji.Width * roopcnt.Value / (ParticipantDropdown.Selected.Value + 1) - 
                            Len(Last(FirstN(ParticipantGallery.AllItems, roopcnt.Value)).ParticipantTextInput.Text) * 10 & "' 
                      y='106' 
                      font-size='20' 
                      fill='white' 
                      font-weight='bold'
                      > " &
                    Last(FirstN(ParticipantGallery.AllItems, roopcnt.Value)).ParticipantTextInput.Text & "
                </text>
            </g>"
        )
    ),
    Value
)

アニメーションはボタンが押されたときに開始したいので、If 関数 を用います。

ボタンを配置して、 OnSelectUpdateContext({isStart:true}); を設定しておいてください。

まずボタンが押されていないとき、つまり、If 関数の判定がfalse のときの記載は先ほどと同様なので省きます。

ボタンが押されたとき、先ほどの軌跡に沿ってアニメーションを行わせたいので

<animateMotion dur='7s' repeatCount='1' fill='freeze' >
    <mpath xlink:href='#theMotionPath " & roopcnt.Value &"' />
</animateMotion>

を追加しています。

<mpath xlink:href='#theMotionPath " & roopcnt.Value &"' /> でどの軌跡に沿ってアニメーションを行うか判断しています。

また、アニメーション終了後終了地点で止まってほしいので、 fill='freeze' としています。

四角形やテキストに設定している座標も異なっていますが、これは先ほど作成した軌跡との相対座標を設定しているからです。

さぁ動かしてみよう!

ではここまでの設定が完了しましたら、ボタンを押して、いざアニメーションを動作させてみましょう!!

・・・

如何でしょうか?
想定通り動きましたか?

恐らくボタンを押すととあみだくじが消えてしまったかと思います。

xmlns:xlink='http://www.w3.org/1999/xlink' を、 svg viewBox に追記してください。

<svg viewBox='0 0 "& Self.Width & " " & Self.Height & "' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink'> 

こちらを追記するとアニメーションが動作します。
気になる人は是非調べてみてください。

おわりに

次回はあみだくじの結果を表示する方法を纏めようと思います。
このシリーズもいよいよ最後です。

もし、SVG が上手く表示されないーという場合はスペースもきちんと記載しているか確認してください。
スペースの設定もSVG の表示には重要な要素です。


スポンサードリンク