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

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

【Power Apps】カスタム関数が作成できるようになりました!

はじめに

2021年2月18日日の発表でカスタム関数の作成機能をプレビューでの公開を開始した。といった旨の記事が公開されました。

powerapps.microsoft.com

この発表によると、これまでは、 OnSelect のイベントのみ実行可能でしたが、今回のアップデートで OnChange イベントを実行できるようになったと記載されています。
例えば、クエリや関数を作成して、それにパラメータを渡すことで内部で計算させて結果を出力させるという関数のようなものを独自に作成できるようになったようですね。

とはいえなにやら制限はあるっぽい?ですが私の英語力と知識が足りないので、なにを伝えたいのかよくわからないですね。。。

such as output properties must be pure without side effects

"出力プロパティは副作用のない純粋なものでなければならない"
と直訳するとなるのですかね?どういうことだ??

なおこの機能はPreview 機能のため、今後更に機能追加が行われていく一方これまで使えていた機能が使えなくなる。仕様変更が行われる。などの可能性があるためご注意ください。

カスタム関数を作成できるようにする

カスタム関数の作成機能は現段階ではPreview 機能なため、設定により対象機能を有効化してあげる必要があります。

ファイル > 設定 > 詳細設定 > 実験的な機能 より、"拡張コンポーネントのプロパティ"を"オン"にします。

f:id:koruneko:20210224000115p:plain

これでカスタム関数作成の準備ができました!

カスタム関数を作成してみる

今回は Enhanced component properties に記載されている例に倣ってカスタム関数を作成していこうと思います。

動作プロパティ

"コンポーネント"より、"新しいコンポーネント"を選択して、コンポーネントを作成します。

f:id:koruneko:20210224001708p:plain

記事のように、"日付の選択"と"ラベル"を2つずつ作成します。

f:id:koruneko:20210224002257p:plain

これから作成するカスタム関数では、"日付の選択が変更されると、通知を行う"といったものを作成するようです。

"Component1"を選択して、"新しいカスタム プロパティ"より、カスタムプロパティを定義します。

f:id:koruneko:20210224002729p:plain

今回設定した項目は以下項目です。
表示名:OnChange
説明:Notify when User Change DataPicker
プロパティの型:動作

設定したら、"作成"を行います。

先ほど作成した"日付の選択"を2つ選択し、 OnChange にて以下のように設定します。

f:id:koruneko:20210224003641p:plain

OnChange

Component1.OnChange()

ここで記載した、 OnChange() は先ほどカスタムプロパティで設定した名前となります。

では実際に作成したカスタム関数の動作確認を行ってみましょう。

画面に戻り、"カスタム"より先ほど作成した Component1 を追加します。

f:id:koruneko:20210224004337p:plain

追加した Component1OnChange プロパティに Notify() 関数を設定します。

f:id:koruneko:20210224004604p:plain

Component1_1.OnChange

Notify("You have changed date.")

それでは日付を変更してみましょう。
日付を変更してみると、先ほど設定した通知が画面上部に表示されたかと思います。

Great! Easy so far.

すごい!今のところ簡単だそうです。

プロパティパラメータ

続いて OnChange の際にパラメータを渡してみます。

先ほど作成したコンポーネントに戻り、カスタムプロパティの編集画面を表示します。
先ほど作成したカスタムプロパティの右側(画像だと"テキスト")を選択すると表示できます。

f:id:koruneko:20210224005416p:plain

"新しいパラメーター"を選択して、カスタム関数にパラメータ(引数)を定義します。

f:id:koruneko:20210224005928p:plain

今回設定した項目は以下項目です。
パラメータ名:Begin
説明:a parameter
データ型:日時

作成を行うと、作成したパラメータが追加されます。

f:id:koruneko:20210224010137p:plain

同様の手順でパラメータ名:End のパラメータも作成しましょう。

f:id:koruneko:20210224010307p:plain

保存を行ったら、"日付の選択"を2つ選択して OnChange プロパティを編集して、呼び出している OnChange 関数に先ほど作成したパラメータを渡すように設定します。

f:id:koruneko:20210224010633p:plain

OnChange

Component1.OnChange(DatePicker1.SelectedDate, DatePicker1_1.SelectedDate)

画面に戻り、作成したパラメータを通知で表示するように設定します。

f:id:koruneko:20210224011008p:plain

Component1_1.OnChange

Notify("You have changed date. Begin is " & Begin & ", End is " & End)

先ほど、 Component1_1OnChange に設定したはずの関数が消えているかと思いますが、これはバグで修正予定だそうです。

Note that because we added the parameters to the event, the formula’s value in the app will have returned to its default and our earlier customization will have been lost. This is a bug that we will fix soon (sorry, we’re still experimental).

設定を行い、実際に動作させてみると、選択した日付が表示されたかと思います。

純粋関数

最後に、入力プロパティと出力プロパティを利用してみます。

Power Apps ではExcelRANDBETWEEN 関数 という、指定された範囲内の整数の乱数を返すような関数は提供されていません。
今回はこのRANDBETWEEN 関数をカスタム関数で作成してみます。

新規で MathUtils というコンポーネントを作成して、 RandBetween というカスタムプロパティを定義します。

f:id:koruneko:20210224012207p:plain

今回設定した項目は以下項目です。
表示名:RandBetween
説明:RandBetween
プロパティの型:出力
データ型:数値

続いてパラメータを2つ作成します。
作成するパラメータは数値型の BottomTop です。
これらは必須パラメータとしたいので、"必須"にチェックを入れます。

f:id:koruneko:20210224013211p:plain

f:id:koruneko:20210224013315p:plain

MathUtils を選択し、設定項目を確認してみると、先ほど作成したカスタム関数を確認することができます。

f:id:koruneko:20210224013447p:plain

RandBetween に以下式を記載します。

MathUtils.RandBetween

If(
    Top >= Bottom, 
    Round(Rand() * (Top - Bottom) + Bottom, 0),
    Blank()
)

簡単に式の解説を記載しておきます。
まず、前提として作成する乱数の範囲は 最大値 >= 最小値 となっている必要があるので Top >= Bottom で判定を行っています。

Round 関数 は、簡単にいうと指定した桁数になるように四捨五入を行う関数です。
Rand 関数 は、0 以上 1 未満の疑似乱数を返す関数です。

Rand() * X とすると、0~X までの値が返ってくることになりますね。
このX が最大値である Top です。 また、最低値は0ではなく Bottom で設定された値としたいです。
なので Bottom の数値を加算するわけですが、これでは Top + Bottom の数値が最大返ってきます。
これでは想定通りの動作にならないので、 Top - Bottom を行っているというわけです。

以上でカスタム関数の作成は完了したので、画面に追加してカスタム関数の動作確認を行ってみます。

画面に先ほど作成した、 MathUtils コンポーネントを追加し、 BottomTop を設定するためのスライダー、 RandBetween 関数の出力結果を表示するためのラベル、また、それらがなにを表すのかを記載したラベルを追加します。

f:id:koruneko:20210224015039p:plain

RandBetween 関数の出力結果を表示するためのラベルには以下のように設定し、 RandBetween 関数を呼び出すようにします。

Text

MathUtils_1.RandBetween(Slider1.Value, Slider1_1.Value)

スライダーの値を変更してみると、Bottom ~ Top までの間でランダムな数値が返されることが確認できるかと思います。

おわりに

発表されたブログの日本語訳と簡単な解説の補足だけになってしまいましたが、以上でカスタム関数の作成の紹介は終わりです。
これを利用すれば、よく使うような式を関数化して使いまわすようにしたり、開発を行う際に関数作成チームと画面作成チームという所謂フロントとエンドに分かれての開発が可能になってくるかもですね。

ただ、ブログでも記載されている通り、いくつか問題があるようです。

  • RandBetween is a pure function – it does its work based purely on its input parameters. It can’t read state in the app, such as global variables or data sources.
  • RandBetween is a data flow property. It cannot change state within the component or the app.
  • The component had to be instanced. It is a UX component that has to be placed on the screen, where in fact MathUtils has no UI at all. It would be better if RandBetween was simply a function in a MathUtils namespace and not object oriented.

We are actively working to build on this foundation, to make this all cleaner, and address the limitations.

私たちは、この基盤上に構築を行い、これをすべてクリーンにして、制限に対処するために積極的に取り組んでいます。

もし利用していく中で、意見などがありましたら、Power Apps community forum で投げてみましょう。

【PowerShell Runbook】 入力パラメータの型検証

はじめに

この記事は、PoweShell Runbook で、入力パラメータにて値を渡したときその値の型はどうなるのか?
を検証した記事になります。

誤っている箇所があればご指摘いただければと思います。

型検証を行おうと思ったきっかけはこちらの記事で、Power Automate からJSON 値を渡したはずがConvertFrom-Json でなぜか失敗してしまったことがきっかけです。
Microsoft 365 ユーザーアカウントの作成を自動化する

PowerShell Runbook での入力パラメータの構成方法

PowerShell Runbook で入力パラメータを構成するには、以下のように記述します。

Param
(
  [Parameter (Mandatory= $true/$false)]
  [Type] $Name1 = <Default value>,

  [Parameter (Mandatory= $true/$false)]
  [Type] $Name2 = <Default value>
)

詳しくは以下の公式doc をご確認ください。

docs.microsoft.com

検証

PowerShell Runbook の作成

今回作成したPowerShell Runbook には以下のような検証用シェルを作成しました。
PowerShell Runbook の詳しい作成方法は今回省きます。

Param
(
    $str_1,[string]$str_2,
    $num_1,[int]$num_2,
    $json_1,$json_2,$json_3
)

$str = "str"
$num = 1
$json = '{ "name": "korune" }'

echo ("str:" + $str)
echo (" - type:" + $str.GetType().FullName)

echo  "`r`n"

echo ("num:" + $num)
echo (" - type:" + $num.GetType().FullName)

echo  "`r`n"

echo ("json:" + $json)
echo (" - type:" + $json.GetType().FullName)

echo  "`r`n"

echo ("str_1:" + $str_1)
echo (" - type:" + $str_1.GetType().FullName)
echo ("str_2:" + $str_2)
echo (" - type:" + $str_2.GetType().FullName)

echo  "`r`n"

echo ("num_1:" + $num_1)
echo (" - type:" + $num_1.GetType().FullName)
echo ("num_2:" + $num_2)
echo (" - type:" + $num_2.GetType().FullName)

echo  "`r`n"

echo ("json_1:" + $json_1)
echo (" - type:" + $json_1.GetType().FullName)
echo ("json_2:" + $json_2)
echo (" - type:" + $json_2.GetType().FullName)
echo ("json_3:" + $json_3)
echo (" - type:" + $json_3.GetType().FullName)

echo  "`r`n"

echo ("`r`n---ConvertFrom-Json----`r`n")

$convert_json = ConvertFrom-Json $json
echo ("json:" + $convert_json)
echo (" - type:" + $convert_json.GetType().FullName)
echo  "`r`n"

$convert_json_1 = ConvertFrom-Json $json_1
echo ("json_1:" + $convert_json_1)
echo (" - type:" + $convert_json_1.GetType().FullName)
echo  "`r`n"

$convert_json_2 = ConvertFrom-Json $json_2
echo ("json_2:" + $convert_json_2)
echo (" - type:" + $convert_json_2.GetType().FullName)
echo  "`r`n"

$convert_json_3 = ConvertFrom-Json $json_3
echo ("json_3:" + $convert_json_3)
echo (" - type:" + $convert_json_3.GetType().FullName)
echo  "`r`n"

echo ("`r`n---ConvertFrom-Json(not in param)----`r`n")

echo ("json")
ConvertFrom-Json $json
echo  "`r`n"

echo ("json_1")
ConvertFrom-Json $json_1
echo  "`r`n"

echo ("json_2")
ConvertFrom-Json $json_2
echo  "`r`n"

echo ("json_3")
ConvertFrom-Json $json_3

ただ実際変数に設定されている値と、その型がわかり、またConvertFrom-Json で失敗するのはどういったケースなのか?
がわかればいいので、書き方は適当です。

テスト実行

それぞれ以下の画像のような変数を設定して実行してみました。

f:id:koruneko:20210217014859p:plain

その際の出力は以下のようになりました。

str:str
 - type:System.String

num:1
 - type:System.Int32

json:{ "name": "korune" }
 - type:System.String

str_1:a
 - type:System.String
str_2:b
 - type:System.String

num_1:1
 - type:System.String
num_2:2
 - type:System.Int32

json_1:{ "name": "korune" }
 - type:System.String
json_2:'{ "name": "korune" }'
 - type:System.String
json_3:[{ "name": "korune" }]
 - type:System.String


---ConvertFrom-Json----
json:@{name=korune}
 - type:System.Management.Automation.PSCustomObject

json_1:@{name=korune}
 - type:System.Management.Automation.PSCustomObject

json_2:{ "name": "korune" }
 - type:System.String

json_3:
 - type:System.Object[]


---ConvertFrom-Json(not in param)----
json

name  
----  
korune

json_1

name  
----  
korune

json_2
{ "name": "korune" }

json_3

Length         : 1
LongLength     : 1
Rank           : 1
SyncRoot       : {@{name=korune}}
IsReadOnly     : False
IsFixedSize    : True
IsSynchronized : False
Count          : 1

番号(_X)が変数の末についていないものは、変数の値をシェル内で定義しています。
変数の末が_1のものは、Param() にて型宣言を行わずに変数を定義しています。
変数の末が_2のものは、Param() にて型宣言を行って変数を定義しています。
* JSON 除く

JSON の型宣言を行っていないのはJSON 型というものが存在しないためです。

ちなみにPowerShell で利用可能な型は以下になります。

Type Name
[array] [System.Array]
[bool] [System.Boolean]
[byte] [System.Byte]
[char] [System.Char]
[datetime] [System.DateTime]
[decimal] [System.Decimal]
[double] [System.Double]
[guid] [System.Guid]
[hashtable] [System.Collections.Hashtable]
[int16] [System.Int16]
[int32][int] [System.Int32]
[int64][long] [System.Int64]
[nullable] [System.Nullable]
[psobject] [System.Management.Automation.PSObject]
[regex] [System.Text.RegularExpressions.Regex]
[sbyte] [System.SByte]
[scriptblock] [System.Management.Automation.ScriptBlock]
[single][float] [System.Single]
[string] [System.String]
[switch] [System.Management.Automation.SwitchParameter]
[timespan] [System.TimeSpan]
[type] [System.Type]
[uint16] [System.UInt16]
[uint32] [System.UInt32]
[uint64] [System.UInt64]
[xml] [System.Xml.XmlDocument]

出力結果を纏めると以下のようになります。

設定値と型の関係

変数名 備考 設定値
str シェル内定義 str System.String
str_1 型宣言なし a System.String
str_2 型宣言あり[string] b System.String
num シェル内定義 1 System.Int32
num_1 型宣言なし 1 System.String
num_2 型宣言あり[int] 2 System.Int32
json シェル内定義 { "name": "korune" } System.String
json_1 型宣言なし { "name": "korune" } System.String
json_2 型宣言なし '{ "name": "korune" }' System.String
json_3 型宣言なし [{ "name": "korune" }] System.String

ConvertFrom-Json の結果

変数名 設定値 出力
json { "name": "korune" } name
----
korune
System.Management.Automation.PSCustomObject
json_1 { "name": "korune" } name
----
korune
System.Management.Automation.PSCustomObject
json_2 '{ "name": "korune" }' { "name": "korune" } System.String
json_3 [{ "name": "korune" }] Length : 1
LongLength : 1
Rank : 1
SyncRoot : {@{name=korune}}
IsReadOnly : False
IsFixedSize : True
IsSynchronized : False
Count : 1
System.Object[]

ConvertFrom-Json の結果はドキュメントを確認すると、 System.Management.Automation.PSCustomObject となっている。
ConvertFrom-Json

json_2json_3 の結果は System.Management.Automation.PSCustomObject ではないが、何故エラーになっていない?
私の知識が足りないので、ここはもう少し調査を行う必要がある。。。

Power Automate からPowerShell Runbook に値を渡してみる

Power Automate からPowerShell Runbook に値を渡すようフローを作成します。
渡す値は先ほどと同じ値を渡したいと思います。

その結果が以下になります。

f:id:koruneko:20210222014457p:plain

json_2 にて "有効な JSON を入力してください。" とでているのはPowerShell 内で ConvertFrom-Json を利用しているのでその解析結果によるものかと思いますが、 str_1 で同様のエラーがでているのは何故??
Object 型から string 型への変換に失敗してこのエラーがでている?? こちらの公式ドキュメントのRunbook の入力パラメーターを構成する - Runbook に JSON オブジェクトを渡すをみてみると、

JSON データを受け入れるには、Runbook は、入力パラメーターとしてオブジェクトを受け取る必要があります

と記載されています。

これは私の推測なので詳細はわかりませんが、この"ジョブの作成"コネクタのエラーハンドリングが適切ではないのでは??と推測しました。
ここは中のソースを確認するなりサポートに確認するなりしないとわからないですね。

必須項目にはしていないので、該当項目を空白にして保存を行ったところ以下エラーが出力されました。

f:id:koruneko:20210222015746p:plain

フローの保存がコード 'OpenApiOperationParameterValidationFailed' およびメッセージ '入力パラメーター 'body' の検証がワークフロー操作 'ジョブの作成' で失敗しました: 種類/形式 'Integer' のパス 'body/properties/parameters/num_1' にある値 '1' のパラメーターは、種類/形式 'Object' に変換できません。' で失敗しました。

先ほどRunbook のテストを行うときにパラメータ入力のスクショを貼り付けましたが、その際、型の宣言を行わなかった変数の型は 'Object' と表示されていたかと思います。

f:id:koruneko:20210222020308p:plain

このことから型のデフォルト設定値は Object だということがわかりますが、公式ドキュメントみるとそもそもPowerShell Runbook の入力パラメーターの Type は必須になっていて、デフォルト値がなにか記載とかないんですよね...
Runbook の入力パラメーターを構成する - PowerShell Runbook の入力パラメーターを構成する

まぁ、ちゃんと型宣言しろよ。ってことかもしれませんが、これPowerShell Runbook 側でエラーとかださなくていいんですかね?もしくはドキュメントの記載を変えるか。
PowerShell の構文としては問題ないので、ドキュメントの記載方法変えたほうがいいような...と思いますが、私がなにか勘違いしているのでしょうかね。

とりあえずこのことからも、型の宣言はきちんと行っておくべきだということがわかりますね。

対象項目を空白にしましたが、問題はまだあります。

f:id:koruneko:20210222021114p:plain

フローの保存がコード 'OpenApiOperationParameterValidationFailed' およびメッセージ '入力パラメーター 'body' の検証がワークフロー操作 'ジョブの作成' で失敗しました: API 操作 'CreateJob' では、プロパティ 'body/properties/parameters/json_3' の種類を 'Object' にする必要がありますが、種類が 'Array' です。' で失敗しました。

指摘の通り、 [{ "name": "korune" }] は配列なのでエラーがでています。
一旦上の3つの項目、

  • str_2
  • num_2
  • json_1

だけを設定して実行してみます。

その結果が以下になります。

str:str
 - type:System.String

num:1
 - type:System.Int32

json:{ "name": "korune" }
 - type:System.String

str_1:
str_2:b
 - type:System.String

num_1:
num_2:1
 - type:System.Int32

json_1:@{name=korune}
 - type:System.Management.Automation.PSCustomObject
json_2:
json_3:

---ConvertFrom-Json----
json:@{name=korune}
 - type:System.Management.Automation.PSCustomObject

json_1:

json_2:

json_3:

---ConvertFrom-Json(not in param)----
json

name  
----  
korune


json_1

json_2

json_3

ConvertTo-Json できてないじゃん...

入力値を確認してみます。

f:id:koruneko:20210222022040p:plain

入力値としては想定通りの値が渡ってきています。

ログをみてみます。

f:id:koruneko:20210222022306p:plain

f:id:koruneko:20210222022330p:plain

Invalid JSON primitive: .

つまりJSON じゃないといわれていますね。

出力結果をみてみると、 json_1 は以下のように出力されています。

json_1:@{name=korune}
 - type:System.Management.Automation.PSCustomObject

このことから、どうやらPower Automate からPowerShell Runbook へJSON を渡したときは ConvertTo-Json が行われた結果が変数に設定されるようです。

これは、shibatea さんがMicrosoft 365 ユーザーアカウントの作成を自動化するのコメントでも記載してくれている通り、Power Automate の"ジョブの作成"のRunbook のパラメーターの型は dynamic (動的) なので、渡したパラメーターを自動的に型解決してくれているのだと思います。

Azure Automation - Connectors | Microsoft Docs

このことから考えてもちゃんと型宣言しておくべきですね。

おわりに

以上、PowerShell Runbook の入力パラメータの型検証結果でした。

私の推測より記載している部分が多々ありますので、あくまでも参考程度にしていただければと思います。
また、誤っている情報がありましたら、指摘していただけると助かります。

【GPPB Virtual Bootcamp 2021 Sapporo】30分で作成!?解説しながら作る横スクアクションゲーム

はじめに

この記事はGPPB Virtual Bootcamp 2021 Sapporo での私のセッション【30分で作成!?解説しながら作る横スクアクションゲーム】の登壇資料です。

本イベントの動画は下記にて公開されておりますので併せてご確認ください!

当日配信URL
こちら よりご確認ください

チャンネルURL

www.youtube.com

横スクアクションゲームの作成

概要

今回作成するゲームは、強制横スクロールを行い、画面をタップするとプレイヤーがジャンプして障害物を避けるというゲームです。

アニメーション部分にはSVG を利用します。
これはPower Apps のコントロールを駆使してアニメーションの描画を行うことも可能ではありますが、どうしてもアイテム数が増えてくると動作がもっさりしてしまい、ゲームをプレイしたときの爽快感が薄れてしまうからです。

ステージを作成する

ステージは画面がスクロールすることによって変化するのでSVG を利用して描画します。

まずは今回利用するSVG の基本形となる画面に四角形の図形を表示するためのSVG をコピペして作成します。
画像コントロールは画面領域いっぱいに表示されるようにします。

StageImage.Image

"data:image/svg+xml,"& 
EncodeUrl(
    "<svg viewBox='0 0 "& Self.Width & " " & Self.Height & "' xmlns='http://www.w3.org/2000/svg'>
        <rect 
            x='"& (Self.Width - 200) / 2 & "' 
            y='" & (Self.Height - 200) / 2 & "' 
            width='200' 
            height='200' 
        />
    </svg>"
)

これで画面中央に黒色の四角形が表示されたかと思います。
これが今回作成するステージの基本形となります。

続いて、ステージが"どこに"、"どれだけの大きさで"存在するかを表した情報を作成します。
この情報は stage というコレクションに設定したいと思います。

開発を簡単にするために、 initButton というボタンを作成して、そのボタンが押されたときに各種変数類が初期化されるようにしておきます。

initButton.OnsSelect

ClearCollect(
    stage,
    {x:0, y:Parent.Height - 150, width:Parent.Width, height:150}
)

initButton.OnsSelect = Screen1.OnVisible となります。
開発が完了したらこのボタンは削除し、設定した式は OnVisibel に記載することにします。

上記の式でステージの初期情報を作成することができました。
なのでステージの見た目もこのコレクションの内容と同じものを表示させるようにしたいと思います。

これを実現するために、コレクションのレコード情報をForAll 関数で取得し、その結果をConcat 関数で文字列の結合を行うようにします。

StageImage.Image

"data:image/svg+xml,"& 
EncodeUrl(
    "<svg viewBox='0 0 "& Self.Width & " " & Self.Height & "' xmlns='http://www.w3.org/2000/svg'>" &
        Concat(
            ForAll(
                stage,
                "<rect 
                    x='" & x &"' 
                    y='" & y &"' 
                    width='" & width &"' 
                    height='" & height &"' 
                />"
            ),
            Value
        ) & "
    </svg>"
)

これで、画面下部にスクリーンの幅いっぱいで高さ150の四角形を作成することができました。

これでステージの作成は完了です。

プレイヤーを表示する

プレイヤーもSVG で表示させます。

今回プレイヤーは複雑なアニメーションをさせないので、普通に画像を読み込んで表示させてもよかったのですが、画像の引用元を明らかにしておきたかった( <metadata> にて引用元を記載しています。)のと、機能の追加でプレイヤーにもアニメーションを適用(ジャンプなどで画像が傾くなど)することを想定してSVG で表示させています。

大きさ100の画像コントロールを追加して、以下を設定します。

PlayerImage.Image

"data:image/svg+xml,"& 
EncodeUrl(
    "<svg version='1.0' xmlns='http://www.w3.org/2000/svg'
 width='1198.000000pt' height='1280.000000pt' viewBox='0 0 1198.000000 1280.000000'
 preserveAspectRatio='xMidYMid meet'>
<metadata>
Created by potrace 1.15, written by Peter Selinger 2001-2017
'https://svgsilh.com/ja/image/2029531.html'
</metadata>
    <g
     transform='matrix(-0.1,0,0,-0.1,1198.3267,1280) rotate(0 0 0)'
     fill='#000000'
     stroke='none'
     >
    <path
       d='m 8235,12784 c -140,-31 -244,-82 -481,-236 -180,-117 -202,-120 -244,-34 -25,54 -36,64 -75,68 -26,3 -31,-1 -46,-42 -24,-60 -23,-103 2,-151 33,-65 27,-102 -31,-211 -28,-54 -61,-105 -73,-113 -12,-8 -39,-15 -60,-15 -42,0 -77,28 -109,88 -34,63 -108,29 -138,-64 -16,-46 -9,-85 29,-172 17,-39 31,-77 31,-84 0,-8 -13,-27 -30,-43 -34,-32 -38,-60 -14,-94 14,-20 23,-22 56,-18 64,9 103,-9 181,-82 40,-37 101,-86 134,-109 111,-76 138,-136 128,-294 l -6,-106 30,-34 c 16,-19 69,-58 118,-88 111,-68 192,-144 232,-220 57,-107 101,-127 206,-94 48,15 55,15 110,0 62,-19 115,-61 115,-94 0,-33 -36,-67 -111,-107 -81,-42 -109,-67 -109,-94 0,-29 47,-79 107,-113 29,-16 53,-36 53,-44 0,-8 -21,-24 -47,-35 -26,-11 -79,-44 -118,-73 -38,-29 -91,-63 -117,-75 -47,-22 -70,-51 -123,-156 -31,-60 -74,-97 -171,-147 -113,-58 -191,-88 -340,-133 -179,-55 -361,-129 -544,-223 -373,-192 -431,-217 -596,-251 -197,-41 -395,-50 -689,-30 -179,12 -210,20 -230,60 -24,46 -18,73 37,155 75,112 127,164 166,164 35,0 62,22 62,51 0,24 -43,69 -65,69 -47,0 -149,-81 -330,-259 -178,-177 -193,-189 -242,-201 -90,-21 -126,-8 -209,74 -41,40 -76,84 -80,100 -9,36 3,84 55,218 58,151 75,241 68,358 -7,118 -33,298 -53,367 -19,70 -137,227 -269,359 -81,81 -116,108 -182,141 -136,68 -216,68 -372,-1 -42,-18 -110,-42 -151,-52 -41,-11 -120,-43 -175,-72 -55,-29 -150,-77 -210,-107 -134,-66 -186,-108 -220,-180 -14,-30 -30,-55 -35,-55 -21,0 -40,-34 -40,-73 0,-40 -41,-175 -90,-299 -22,-57 -23,-58 -6,-163 9,-58 19,-147 23,-198 7,-101 -3,-85 106,-179 35,-30 39,-39 47,-102 10,-80 33,-118 78,-127 71,-14 100,-51 121,-154 25,-119 66,-162 231,-245 95,-47 190,-112 235,-160 42,-44 96,-70 149,-70 37,0 53,7 103,46 136,106 180,124 254,104 22,-6 60,-28 82,-49 23,-21 62,-51 87,-68 111,-75 171,-173 197,-321 14,-75 14,-111 4,-257 -7,-93 -16,-240 -21,-325 -11,-184 -32,-274 -95,-408 -61,-128 -158,-284 -200,-321 -19,-16 -51,-33 -72,-37 -56,-10 -562,4 -608,17 -37,11 -51,23 -143,126 -43,47 -127,69 -201,53 -53,-12 -62,-19 -191,-150 -111,-113 -144,-140 -185,-155 -108,-38 -173,-78 -273,-166 -57,-50 -112,-105 -123,-123 -23,-36 -94,-261 -103,-324 -20,-136 61,-298 198,-399 77,-57 123,-80 263,-133 160,-59 203,-70 336,-80 65,-5 125,-14 133,-20 34,-29 20,-90 -103,-465 -118,-361 -196,-542 -251,-585 -34,-27 -60,-25 -187,15 -556,178 -1137,86 -1668,-263 C 1007,4101 893,4007 757,3872 372,3491 121,3016 29,2493 -4,2312 -4,1981 28,1806 132,1242 424,797 956,397 1163,242 1248,197 1481,122 1706,49 1914,13 2165,3 c 471,-18 950,108 1347,354 500,309 858,785 997,1326 54,208 66,317 62,567 -2,124 -4,270 -5,325 l -1,101 38,51 c 42,58 60,105 77,208 24,138 88,211 208,236 144,29 338,-74 409,-219 23,-47 28,-70 28,-132 0,-87 -22,-138 -79,-187 -39,-35 -74,-48 -164,-62 -163,-26 -196,-107 -138,-341 44,-177 35,-221 -65,-328 -64,-68 -98,-129 -99,-173 0,-73 52,-128 108,-114 39,10 275,129 340,172 31,21 88,68 127,104 73,69 102,89 131,89 9,0 55,-17 103,-39 217,-97 426,-119 926,-97 278,12 281,10 305,-149 34,-227 195,-552 400,-810 203,-254 491,-492 770,-634 465,-237 1026,-301 1556,-176 706,166 1315,672 1597,1325 120,278 166,489 174,796 6,220 -4,339 -42,522 -126,596 -520,1146 -1070,1493 -119,74 -326,177 -436,215 -44,15 -85,34 -90,41 -8,9 -8,45 1,130 37,342 -51,701 -238,967 -33,48 -55,90 -55,105 4,159 17,254 89,636 15,80 45,145 80,176 11,9 62,40 114,69 52,28 169,101 260,162 269,181 287,190 710,350 856,323 974,379 1030,489 17,33 20,59 20,186 -1,617 -262,1099 -830,1532 -217,165 -443,308 -653,412 -127,64 -151,85 -127,114 25,30 109,18 333,-50 360,-107 503,-135 701,-135 201,0 268,29 302,133 24,72 40,94 81,108 93,30 232,-9 352,-100 30,-22 62,-41 72,-41 10,0 27,12 39,28 19,23 22,35 17,92 -11,125 -4,123 -329,118 -296,-4 -303,-6 -389,-98 -55,-59 -57,-60 -109,-60 -112,0 -617,99 -791,156 -62,20 -104,64 -95,99 4,15 18,38 31,51 23,23 29,24 178,24 153,0 155,0 278,41 68,22 159,56 201,75 101,44 307,175 363,231 56,55 110,162 100,195 -5,12 -38,55 -76,93 -45,47 -81,97 -108,150 -24,48 -65,106 -102,145 -33,36 -68,79 -77,95 -22,43 -31,102 -21,140 13,47 132,154 211,191 329,153 445,236 554,399 39,58 52,125 35,178 -19,57 -75,62 -101,9 -62,-123 -199,-273 -326,-355 -39,-25 -112,-62 -163,-83 -167,-67 -288,-169 -352,-299 -55,-112 -50,-191 15,-277 47,-61 73,-115 97,-205 27,-101 31,-107 100,-180 67,-70 73,-96 32,-136 -40,-38 -114,-68 -328,-132 -105,-31 -252,-81 -327,-111 -76,-30 -142,-54 -147,-54 -29,0 -157,-52 -193,-79 -52,-39 -89,-40 -117,-5 -31,40 -27,91 14,170 38,73 44,117 19,152 -23,33 -58,27 -140,-24 -91,-56 -106,-74 -172,-201 -93,-175 -113,-180 -223,-45 -40,48 -128,144 -196,213 -145,148 -147,155 -74,217 27,23 52,51 56,62 9,29 -12,60 -45,67 -46,9 -64,37 -70,107 -5,56 -7,61 -30,64 -20,2 -45,-15 -116,-81 -50,-46 -103,-87 -117,-91 -95,-24 -183,117 -194,311 -10,160 36,224 223,321 56,28 107,59 115,68 26,31 91,54 157,54 78,0 118,18 172,76 33,36 41,51 38,77 -3,32 -3,32 -66,35 -81,4 -87,18 -33,77 56,61 55,85 -9,131 -27,20 -50,42 -50,49 0,8 29,28 64,45 70,33 99,68 111,131 14,77 -42,179 -98,179 -13,0 -52,-19 -86,-42 -79,-53 -134,-58 -179,-15 -16,15 -34,38 -40,50 -6,12 -35,74 -65,138 -46,97 -65,124 -115,170 -67,62 -183,126 -287,159 -93,29 -254,36 -350,14 z m -96,-3551 c 95,-47 123,-115 102,-238 -27,-158 -35,-397 -23,-667 42,-888 211,-1647 460,-2061 89,-147 90,-148 81,-502 -4,-160 5,-221 93,-580 47,-189 52,-222 52,-326 1,-112 0,-117 -29,-165 -43,-69 -112,-114 -176,-114 -28,0 -64,6 -82,14 -40,16 -95,77 -103,114 -21,99 -36,105 -263,112 -239,7 -235,5 -309,155 -29,60 -79,168 -109,239 -30,72 -92,217 -138,321 -46,105 -90,212 -99,238 -36,106 -12,154 89,182 33,9 101,35 150,57 141,63 180,56 231,-44 26,-52 41,-58 72,-30 43,38 56,153 23,208 -24,40 -86,70 -183,88 -135,26 -151,51 -117,178 38,143 48,210 49,315 0,177 -44,321 -146,476 -30,44 -54,86 -54,93 0,6 8,22 19,35 15,19 27,23 72,22 79,-2 91,3 87,39 -3,28 -8,32 -73,52 -169,54 -186,88 -117,232 33,71 37,87 37,159 -1,93 -15,142 -51,176 -23,22 -36,24 -152,29 -126,6 -128,6 -212,50 -125,65 -349,219 -562,384 -103,80 -229,174 -280,209 -99,67 -115,92 -82,128 30,33 140,59 359,84 421,49 616,105 923,266 83,44 178,86 210,94 83,21 182,13 251,-22 z M 5565,7315 c 50,-30 121,-68 158,-84 167,-75 293,-170 328,-246 13,-28 19,-64 19,-118 0,-88 17,-139 52,-158 12,-7 52,-15 88,-18 59,-5 65,-8 68,-30 2,-12 1,-34 -2,-47 -6,-24 -110,-109 -217,-177 -31,-20 -94,-68 -140,-107 -46,-39 -122,-94 -169,-122 -47,-28 -107,-72 -133,-97 l -48,-46 -277,-4 c -242,-3 -664,11 -729,25 -40,8 -28,48 50,167 87,133 126,201 282,492 65,121 142,254 173,295 61,83 289,315 324,330 37,15 76,3 173,-55 z M 4218,6678 c 19,-19 14,-108 -9,-178 -69,-210 -241,-446 -372,-509 -38,-18 -62,-21 -183,-21 -190,0 -227,12 -311,96 -35,35 -70,64 -77,64 -7,0 -28,-10 -47,-23 -19,-13 -52,-36 -73,-50 -113,-78 -276,18 -276,161 0,64 23,106 97,184 156,161 356,250 623,277 138,14 614,13 628,-1 z M 2740,6375 c 8,-10 10,-31 5,-64 -14,-100 37,-225 127,-313 60,-59 126,-90 257,-124 97,-25 108,-26 176,-14 63,11 76,10 103,-4 65,-34 71,-99 14,-156 -51,-51 -101,-64 -227,-57 -241,14 -465,94 -565,204 -99,108 -118,234 -61,406 28,83 38,102 65,118 37,23 89,25 106,4 z m 1700,-510 c 36,-19 44,-48 36,-119 -7,-51 -5,-57 23,-88 28,-32 33,-33 113,-36 46,-2 132,3 191,12 59,9 132,16 163,16 65,0 118,20 134,51 8,15 11,45 7,86 -8,81 3,88 117,79 158,-13 162,-39 32,-234 -102,-154 -123,-208 -111,-290 14,-100 82,-207 178,-280 47,-36 145,-93 312,-180 17,-9 52,-17 80,-18 65,-4 75,-19 75,-110 0,-107 -17,-169 -70,-264 -76,-133 -100,-249 -120,-565 -6,-99 -18,-211 -26,-250 -8,-38 -28,-140 -44,-225 -51,-266 -68,-317 -109,-340 -32,-16 -39,-13 -94,47 -76,85 -192,205 -812,848 -674,698 -684,709 -816,858 -57,66 -124,141 -148,168 -24,28 -43,59 -43,72 3,101 21,257 31,277 37,69 77,90 196,100 63,5 106,15 136,30 85,43 288,231 324,299 12,24 49,46 105,65 33,10 113,5 140,-9 z m 2955,-57 c 33,-15 54,-51 103,-173 63,-156 162,-375 251,-552 44,-90 81,-170 81,-178 0,-9 7,-29 14,-44 13,-25 13,-31 0,-50 -12,-17 -25,-21 -68,-21 -76,0 -198,34 -291,80 -98,49 -371,243 -380,270 -4,13 5,44 24,84 17,34 52,118 77,187 25,68 56,145 70,172 19,38 24,64 24,121 0,59 4,76 20,94 24,25 38,27 75,10 z m -275,-88 c 7,-44 -24,-190 -60,-292 -45,-125 -124,-228 -173,-228 -11,0 -46,18 -78,39 -67,45 -146,77 -236,96 -34,7 -66,16 -72,20 -14,8 -14,60 -1,85 12,22 68,40 126,40 64,0 146,48 289,170 116,98 134,109 165,107 32,-2 35,-5 40,-37 z M 3495,4934 c 9,-4 30,-34 47,-68 57,-114 110,-178 352,-429 350,-363 465,-484 614,-649 73,-81 162,-173 198,-205 63,-55 134,-154 134,-186 0,-20 -104,-56 -157,-54 -50,2 -87,-27 -97,-75 -8,-40 -85,-118 -117,-118 -40,0 -144,120 -233,269 -174,290 -395,502 -798,764 -80,52 -154,107 -164,122 -17,26 -16,31 50,248 37,122 73,238 78,257 13,44 36,90 56,113 17,20 15,19 37,11 z m 3148,-476 c 87,-18 86,-36 -27,-301 -91,-216 -197,-511 -242,-675 -19,-69 -48,-158 -63,-197 -15,-38 -31,-88 -35,-110 -11,-57 -50,-120 -93,-146 -28,-17 -56,-24 -118,-27 -106,-6 -130,5 -121,56 10,67 103,233 191,346 47,60 102,143 122,185 20,42 56,103 80,136 57,78 99,165 119,251 21,86 23,278 5,392 -18,108 -11,117 74,107 33,-3 81,-11 108,-17 z m 464,-33 c 158,-20 288,-48 425,-91 224,-71 224,-105 3,-269 -158,-117 -254,-205 -362,-330 -218,-253 -360,-529 -414,-806 -10,-55 -19,-115 -19,-134 0,-67 -48,-82 -137,-41 -38,18 -59,36 -79,68 -14,24 -44,60 -66,80 l -40,38 4,67 c 3,43 16,100 36,153 17,47 54,157 83,245 28,88 78,219 110,292 32,73 78,204 104,295 66,233 125,410 143,431 19,21 55,22 209,2 z m 1981,-147 c 6,-7 26,-87 46,-178 30,-143 60,-296 158,-810 25,-135 27,-281 4,-335 -31,-72 -68,-115 -155,-178 -74,-54 -92,-62 -128,-61 -53,1 -70,15 -83,68 -30,118 -69,227 -135,376 -40,91 -105,240 -144,333 l -71,167 v 299 c 0,259 2,302 16,315 13,14 50,16 248,16 175,0 236,-3 244,-12 z m -6448,-91 c 166,-36 224,-57 228,-84 2,-11 -13,-54 -33,-93 -20,-40 -52,-118 -72,-174 -19,-55 -96,-234 -170,-396 -75,-162 -155,-339 -178,-393 -24,-53 -70,-143 -104,-200 -33,-56 -78,-136 -99,-177 l -39,-75 -81,-5 c -61,-4 -86,-10 -103,-25 -32,-29 -49,-77 -49,-140 0,-48 4,-59 34,-93 19,-20 39,-56 45,-80 22,-81 -29,-76 801,-85 464,-5 847,-4 1040,2 311,11 379,7 408,-23 37,-37 0,-310 -69,-517 C 4017,1076 3551,616 2970,415 2735,334 2555,305 2290,306 2012,307 1795,349 1555,447 685,802 184,1742 371,2664 c 56,277 196,573 377,801 297,372 736,640 1187,724 157,29 159,29 385,26 173,-3 223,-7 320,-28 z m 7237,-58 c 471,-244 828,-639 1043,-1154 47,-115 140,-399 140,-431 0,-12 -8,-32 -19,-45 -26,-32 -132,-45 -506,-60 -274,-11 -309,-10 -410,6 -60,10 -144,20 -185,22 -46,3 -81,11 -92,20 -40,35 -118,304 -145,497 -23,164 -27,1153 -4,1179 24,30 75,20 178,-34 z m -1837,-79 c 27,-15 328,-420 399,-538 138,-230 315,-580 367,-728 16,-45 16,-55 4,-85 -19,-44 -84,-109 -134,-133 -35,-16 -49,-17 -145,-7 -58,6 -187,15 -286,21 -210,12 -269,19 -545,65 -113,19 -259,41 -325,50 -271,35 -337,85 -301,224 18,69 137,315 216,445 66,110 178,261 259,348 107,116 291,261 416,327 45,24 50,25 75,11 z M 3307,3944 c 168,-101 275,-184 423,-333 186,-187 304,-359 406,-591 134,-304 200,-614 144,-670 -19,-19 -33,-20 -522,-19 -848,2 -1289,12 -1315,29 -43,28 -47,60 -20,141 68,202 322,738 612,1290 103,195 118,219 145,219 9,0 67,-30 127,-66 z M 6088,2870 c 69,-42 75,-110 27,-306 -25,-107 -41,-129 -101,-140 -76,-14 -180,60 -204,145 -18,64 -24,187 -10,220 11,28 56,53 170,94 33,12 87,6 118,-13 z m 345,-121 c 50,-26 67,-51 67,-99 0,-137 -35,-200 -111,-200 -78,0 -129,37 -129,92 0,40 47,168 71,194 25,26 66,31 102,13 z m 864,-129 c 333,-65 546,-97 838,-125 84,-8 159,-17 168,-21 10,-3 17,-16 17,-29 0,-50 8,-50 -594,-50 -503,0 -569,2 -636,18 -108,25 -105,22 -105,123 1,78 3,88 24,105 32,26 52,25 288,-21 z m 2333,-316 c 86,-12 221,-15 754,-13 619,2 649,2 658,-16 23,-41 -5,-255 -62,-475 -219,-847 -887,-1437 -1732,-1529 -458,-50 -935,70 -1345,338 -249,163 -433,344 -586,576 -98,149 -267,515 -267,579 0,35 31,64 77,71 21,3 92,23 158,45 163,54 1182,304 1563,384 67,14 155,26 195,27 39,0 117,9 172,19 113,22 236,20 415,-6 z m -3095,-34 c 20,-19 25,-33 25,-74 0,-49 -2,-52 -62,-109 -92,-87 -188,-156 -226,-163 -20,-4 -49,0 -75,10 -29,11 -71,15 -134,14 -81,-1 -98,2 -147,26 -80,39 -118,95 -83,124 22,19 381,154 472,178 114,31 193,28 230,-6 z m 1589,14 c 24,-9 19,-51 -8,-69 -13,-9 -75,-27 -137,-40 -63,-13 -222,-50 -354,-84 -256,-64 -452,-105 -551,-116 -46,-5 -63,-3 -72,8 -7,8 -17,59 -22,113 -13,130 -13,143 5,171 l 15,23 h 554 c 305,0 561,-3 570,-6 z M 6741,2184 c 8,-9 15,-51 17,-103 4,-82 3,-88 -22,-118 -55,-66 -129,-63 -169,5 -18,31 -18,35 -3,75 19,51 68,113 110,138 38,23 50,24 67,3 z'/>
</g>
</svg>"
)

以上でプレイヤーの表示は完了です。

重力を作成する

重力を作成して、プレイヤーが時間経過によって、下に落ちるようにします。

今回時間経過処理にはスライダーを用います。
ゲーム実行中はスライダーコントロールOnChange が呼ばれるようにしてループ処理を実現します。

なので、スライダーコントロールを追加してください。

まずは変数を初期化します。
今回追加する初期化変数は、"プレイヤーの座標" PlayerX, PlayerY 、"スライダーの値" sliderVal 、"ゲーム開始を判定する変数" playGame です。

InitButton.OnSelect

UpdateContext({PlayerX:50, PlayerY:470});
UpdateContext({sliderVal:0});
UpdateContext({playGame:false});

PlayerX, PlayerYPlayerImageXY にそれぞれ設定しておき、 sliderVal はスライダーコントロールDefault に設定しておきましょう。

ゲームの開始はStart ボタンが押されたら開始することにします。
ボタンを追加して以下のように設定します。

StartButton.OnSelect

If(
    playGame,
    UpdateContext({playGame:false});
    Reset(Slider1),
    UpdateContext({sliderVal:sliderVal + 1});
    UpdateContext({playGame:true})
)

ゲームがまだ開始していない場合は、スライダーの OnChange を呼び出すようにして、ゲームが開始している場合にボタンが押されたときはゲームを中断するようにしています。

わかりやすいよう、ボタンに表示される文字も変更しておきましょう。

StartButton.Text

If(!playGame,"Start","Stop")

最後に重力を作成します。
重力 = プレイヤーが下方向に移動する。
なので、 OnChange 処理で PlayerY の値を増やしてあげればよいです。

Slider1.OnChange

If(
    playGame,
    Reset(Slider1);
    UpdateContext({PlayerY:PlayerY + 10});
)

これで重力を実装することができました。
ただこのままでは、プレイヤーがステージを貫通して落ちてしまうので、プレイヤーとステージとの当たり判定を作成します。

プレイヤーとステージとの間に当たり判定を実装する

プレイヤーとステージとの当たり判定にはコントロール名、切り替えのトグルを利用します。

このトグルの値が True の場合がプレイヤーとステージは接触していると判定することにしたいと思います。

当たり判定は以下のようにPlayer とStage を定義したとき

f:id:koruneko:20210214163618p:plain

以下のような式であらわすことができます。

((PlayerX > StageX && PlayerX < StageX + StageWidth) ||
(StageX > PlayerX && StageX < PlayerX + PlayerWidth))  &&
((PlayerY > StageY && PlayerY < StageY + StageHeight) ||
(StageY > PlayerY && StageY < PlayerY + PlayerHeight))

まず2つの図形が"重なっている"というのは、下図のようにX成分でみたときもY成分でみたときも重なっている。
というのが条件になります。

f:id:koruneko:20210214164617p:plain

上の式でいいますと、
この式がX成分の重なり判定で

((PlayerX > StageX && PlayerX < StageX + StageWidth) ||
(StageX > PlayerX && StageX < PlayerX + PlayerWidth))

この式がY成分の重なり判定です。

((PlayerY > StageY && PlayerY < StageY + StageHeight) ||
(StageY > PlayerY && StageY < PlayerY + PlayerHeight))

これら2つの条件を満たしているとき、重なっていると判断できるので && でそれぞれの式を繋げているわけです。

ただし、今回この当たり判定を実装するうえで1点注意しなくてはいけないことがあります。

今回当たり判定を実装したいのは、プレイヤーとステージが接しているか?です。
これをもっと細かく定義すると、
"プレイヤーの下部"と"ステージの上部"が接しているか?
です。

なぜ、ここまで細かく定義するか?というと、上式のまま判定を作成してしまうと、プレイヤーがステージの横方向から突っ込んだ場合でもプレイヤーとステージが接している。と判定してしまうことになるからです。
これはこのゲームを作成するうえで意図した実装ではないので、"プレイヤーの下部"と"ステージの上部"が接しているか?で判定式を作成したいと思います。

これらを踏まえ判定式を作成すると以下のようになります。

isGrand.Default

CountIf(
    ForAll(
        stage,
        ((PlayerImage.X > x && PlayerImage.X < x + width) ||
        (x > PlayerImage.X && x < PlayerImage.X + PlayerImage.Width))  &&
        ((PlayerImage.Y + PlayerImage.Height - 5 > y && PlayerImage.Y + PlayerImage.Height - 5 < y + 5) ||
        (y > PlayerImage.Y + PlayerImage.Height - 5 && y < PlayerImage.Y + PlayerImage.Height))
    ),
    Value = true
) > 0

プレイヤー、ステージそれぞれ高さで定義しています。
プレイヤーの場合、Y座標も調整が必要ですので、そのことも式に盛り込んでいます。

また、判定を行うのはコレクションの存在するステージすべてですので ForAll 関数を用いており、うち1つでも True になれば、ステージのどれかにプレイヤーが接しているということなので、 CountIf 関数で0より大きい場合という条件を指定しています。

プレイヤーがステージと接している場合、プレイヤーのY 座標を加算してはいけないので、重力を設定していた式を以下のように編集します。

Slider1.OnChange

If(
    playGame,
    Reset(Slider1);
    If(!isGrand.Value, UpdateContext({PlayerY:PlayerY + 10}));
)

ジャンプ機能を追加する

画面をクリック(タップ)したときにプレイヤーがジャンプするようにしたいと思います。

ジャンプ機能を追加するにあたり、今回は以下の仕組みを使用しようと思います。

  • ジャンプができる条件:プレイヤーが地面にいる場合。
  • 画面が押されたら:ジャンプができる状態だった場合、変数に一定数値(今回は15)を加算する。
  • ループ処理:変数の値が0より大きければ上昇させる。変数の値を減算する。

これらの条件を満たすことにより、

  • 2重ジャンプの禁止
  • プレイヤーが上昇し続けるバグを抑制

することが可能となります。

では早速実装を行っていきます。

まずは変数の宣言です。

ジャンプをしていない状態 = 0 とするので以下のように宣言します。

InitButton.OnSelect

UpdateContext({PlayerJump:0});

画面が押されたときの処理は、ステージの画像を最終的に最前面に持ってくる StageImage.OnSelect に記載します。

StageImage.OnSelect

If(isGrand.Value, UpdateContext({PlayerJump:15}))

ループ処理はスライダーコントロールOnChange に該当するので、以下のように設定します。

Slider1.OnChange

If(
    playGame,
    Reset(Slider1);
    If(!isGrand.Value && PlayerJump < 0, UpdateContext({PlayerY:PlayerY + 10}));
    If(PlayerJump >= 0, UpdateContext({PlayerY:PlayerY - 10, PlayerJump:PlayerJump - 1}));
)

これで先に述べた機能を満たすことができましたので、ジャンプ機能の作成完了です!

横スクロール機能を作成する

横スクロールはプレイヤーの座標を変更するのではなく、ステージをプレイヤーの進行方向とは反対方向に動かすことで横スクロールしているように見せたいと思います。
* 初回移動時だけプレイヤーを動かします。

プレイヤーは画面左から右へ進んでいるようにみせたいので、反対にステージは右から左へ移動させる。つまりx 座標を減らしてあげればよいです。

これは下記のようにすることで実現可能です。

If(PlayerX > Parent.Width / 3 || !isGrand, UpdateIf(stage, true, {x:x - 10}), UpdateContext({PlayerX:PlayerX + 5}));

PlayerX > Parent.Width / 3 || !isGrand これはゲーム開始時はプレイヤーを画面の1 / 3進めるための処理です。
初回地面と接触するまではプレイヤーのx 成分を動かさないようにしています。
これはこのほうが動きが自然にみえるな。と考えたためです。

ただこのまま減らしただけでは初期で宣言したステージの幅より先はなにもないので、ゲームが終了してしまいます。

なのでステージの自動生成も追加したいと思います。

ステージを自動生成するタイミング = ステージとステージ間の溝

になります。

これは最大でもプレイヤーがギリギリジャンプ可能な位置からジャンプして次のステージまで到達可能な幅にしなくてはいけません。

この計算には、

  • プレイヤーのジャンプ力
  • プレイヤーのx 方向への進む速度
  • ステージ間の高さの差

が分かれば、厳密に計算を行おうと思えば行うことも可能ですが、今回は感覚で設定しています。

また、生成されるステージはランダム性を持たせたいので、 Rand 関数を用います。

ステージのy 座標の値は高さによって決定するので、高さの値はWith 関数で先に決定しておきます。

また、ステージがだんだんずれていくので、コレクション内には不要になったレコードがでてきます。
性能面も考えてこの不要なレコードは削除するようにしましょう。
不要なレコードというのは、画面外にでたステージですね。

上記を踏まえて式を追加すると、以下のようになります。

Slider1.OnChange

If(
    playGame,
    Reset(Slider1);
    If(!isGrand.Value && PlayerJump < 0, UpdateContext({PlayerY:PlayerY + 10}));
    If(PlayerJump >= 0, UpdateContext({PlayerY:PlayerY - 10, PlayerJump:PlayerJump - 1}));
    If(PlayerX > Parent.Width / 3 || !isGrand, UpdateIf(stage, true, {x:x - 10}), UpdateContext({PlayerX:PlayerX + 5}));
    If(
        Parent.Width - (Last(stage).x + Last(stage).width) > 200 + Rand() * 50,
        With(
            {H:Last(stage).height + Rand() * 50 * If(Last(stage).height > 130, If(Rand() > 0.5, 1, -1), 1)},
            Collect(
                stage,
                {
                    x:Parent.Width,
                    y:Parent.Height - H,
                    width:200 + Rand() * 200,
                    height:H
                }
            )
        )
    );
    RemoveIf(stage, x + width < 0)
)

高さを設定している箇所は以下です。

H:Last(stage).height + Rand() * 50 * If(Last(stage).height > 130, If(Rand() > 0.5, 1, -1), 1)

この式を日本語にすると、「追加前のステージの高さが130よりも大きかった場合、50%の確率で最大で高さを50増減させます。130よりも小さかった場合は最大50増やします。」となります。

いくらランダムとはいえ画面外にステージが形成されてはいけないからですね。

スコア表示を行う

ゲーム性を高めるためにスコア表示を行おうと思います。

この機能の実装は簡単にユーザーがゲームをプレーしている間だんだん加算される仕組みにしたいと思います。

InitButton.OnSelect

UpdateContext({Score:0});

Slider1.OnChange(ゲーム実行中に処理されるif 文内に作成)

UpdateContext({Score:Score + 50});

この変数の値を画面に表示させてあげましょう。

ScoreLabel.Text

"Score:" & Score

ゲームオーバー画面を作成する

このゲームの終了条件はプレイヤーが画面外へ落ちてしまうことだけで、クリア条件はありません。
よって今回作成するのはゲームオーバー画面のみです。

ゲームオーバーの条件は、プレイヤーが完全に画面外にでたときとしたいと思います。

地面に接しているときの判定と同じように判定にはトグルを利用します。

トグルの Default に以下の式を適用します。

isGameOver.Default

PlayerY > Parent.Height

ゲームオーバーとなった場合、ゲームの実行を止めたいので以下のように設定します。

isGameOver.OnCheck

UpdateContext({playGame:false});
Reset(Slider1)

これでシステム的にはゲームオーバー判定となりましたが、ゲームをプレーしているユーザーにもゲームオーバーとなったことを知らせてあげましょう。

GameOverLabel.Text

"Game Over..."

GameOverLabel.Visible

isGameOver.Value

このラベルを選択したら、再度ゲームがプレーできる状態としたいため以下のように初期化処理を行うようにします。

GameOverLabel.OnSelect

UpdateContext({PlayerX:50, PlayerY:470});
UpdateContext({sliderVal:0});
UpdateContext({playGame:false});
UpdateContext({PlayerJump:0});
UpdateContext({Score:0});
ClearCollect(
    stage,
    {x:0, y:Self.Height - 150, width:Self.Width, height:150}
)

表示順やVisible を調整

最後に不要なコントロールを非表示に設定します。
不要 = ユーザーには表示する必要がないコントロール
ですので、以下コントロールを非表示にします。

  • Slider1 (スライダー)
  • isGrand (切り替え)
  • isGameOver (切り替え)

また、開発用に作成していた initButtonOnSelect の値を Screen1.OnVisible に移して削除しておきます。

また、コントロールはそれぞれ以下の順序になるように設定します。(値が小さいものが前面)

  1. GameOverLabel
  2. StartButton
  3. StageImage

他コントロールの順序はこれより背面に設定されているのであれば、特に気にしなくて大丈夫です。

以上で横スクアクションゲームの作成完了です!!

おわりに

もし作成してみてわからなかったことや、こんな機能追加したいがどうすればできるか?
などありましたらご気軽にお尋ねくださいー。

Microsoft 365 ユーザーアカウントの作成を自動化する

はじめに

今回は、Microsoft 365 ユーザーアカウントの作成の自動化方法を纏めたいと思います。

ケースとしては、新入社員や中途社員が増えた時や派遣社員の方やBPの方がプロジェクトに参入したときなどにMicrosoft 365 ユーザーアカウントを発行しなくてはならない。
などがあるかと思います。

よって今回のシナリオは以下となるように作成します。

  1. ユーザーの作成を依頼
  2. その依頼を担当者が承認
  3. Microfot 365 ユーザーアカウントを作成
  4. 依頼を行った相手に作成したアカウント情報を払い出し

を想定して作成します。

ユーザー作成依頼フォームを作成する

ユーザー作成を依頼するためのフォームの作成方法として、今回のケースで最も単純で簡単なものは Microsoft Forms かと思います。
ただMicrosoft Forms ですと、ユーザーが入力項目を動的に増やすということを行うことができません。
なので今回はユーザー作成依頼フォームをPower Apps で作成してみようと思います。

* この記事では私がPower Apps が好きなので入力フォームをPower Apps で作成することにしていますが、もちろんMicrosoft Forms でも十分運用可能なレベルかと思います。
  実際に運用される際は必要な機能や作成にかかる労力などから費用対効果を十分に考えたうえで作成することをお勧めします。

Power Apps でユーザー作成フォームを作成する

作成する入力フォームは以下のようなイメージです。

f:id:koruneko:20210131151838p:plain

+ ボタンを押すと以下のように入力フォームが増えて、ごみ箱のアイコンを押すと対象の入力フォームが消えます。
Request ボタンは全ての入力項目が入力されていないと押せない仕様にしています。

f:id:koruneko:20210131152036p:plain

今回このフォームの作成はメインでないため簡単に説明します。

まず入力フォームとなるギャラリーですが、以下の要素で構成されています。

f:id:koruneko:20210131152208p:plain

このコレクションに設定する ItemsApp.OnStart で定義しています。

App.OnStart

ClearCollect(
    userRequest,
    {
        ID:1,
        DisplayName:"",
        UserPrincipalName:"",
        FirstName:"",
        LastName:""
    }
)

ギャラリー内に設置されたテキストボックスは、コレクション userRequest と同期させたいです。
なのでテキストボックスの値が変更されたら都度それぞれ対応する項目を更新するようにします。

TextInput.OnChange

UpdateIf(userRequest, ID = ThisItem.ID, {LastName:Self.Text})

続いて削除アイコンです。
これは

  • 対象の項目のみを削除
  • 入力項目が1つしかない場合は非表示

としたいです。
なので以下のように設定します。

DeleteIcon.OnSelect

Remove(userRequest, ThisItem)

DeleteIcon.Visible

CountRows(userRequest) > 1

これでギャラリー内の要素の作成は完了です。

続いて入力項目を増やすためのボタンの作成です。
これはコレクションを増やすだけでよいので、以下のように設定します。

AddIcon.OnSelect

Collect(
    userRequest,
    {
        ID:Last(userRequest).ID + 1,
        DisplayName:"",
        UserPrincipalName:"",
        FirstName:"",
        LastName:""
    }
)

最後にリクエストボタンを作成します。
これには以下機能を実施します。

  • 入力項目が全て入力されていないときは非活性
  • ボタンが選択されると申請を行うフローを起動して、完了画面へ遷移

「入力項目が全て入力されていないときは非活性」というのは以下で実現できます。

RequestButton.DisplayMode

If(
    CountIf(
        ForAll(
            userRequest,
            IsBlank(DisplayName) || IsBlank(UserPrincipalName) || IsBlank(FirstName) || IsBlank(LastName)
        ),
        Value = true
     ) = 0,
     DisplayMode.Edit,
     DisplayMode.Disabled
)

「ボタンが選択されると申請を行うフローを起動して、完了画面へ遷移」ですが、フローの作成は後の章で行うのでここでは説明を一旦割愛します。
完了画面への遷移の作成は簡単です。

まず完了画面の作成ですが、これはテンプレートを用いましょう。

f:id:koruneko:20210131153606p:plain

「新しい画面」 > 「成功」を選択します。
これだけでそれっぽい画面の完成です。

f:id:koruneko:20210131153702p:plain

あとはボタンが選択されたときの動作にこの画面への遷移を定義するだけでよいので、

RequestBotton.OnSelect

Navigate(Screen2)

以上で入力フォームの作成は完了です。

ユーザー作成処理を作成する

Azure Automation

ここは以前紹介した、【Microsoft Teams】申請されたユーザーを指定のTeams のチームに自動で追加する と一部説明が被りますが再度紹介します。

Azure Automation のデプロイ

  1. Azure Portal にログインを行い、「リソースの作成」を選択します。
    f:id:koruneko:20200719174315p:plain

  2. 「オートメーション」を作成します。
    f:id:koruneko:20200719174423p:plain

  3. 必要項目を入力します。
    f:id:koruneko:20200719174647p:plain

    • 名前:任意の名前を入力します。
    • サブスクリプション:任意のサブスクリプションを選択します。
    • リソースグループ:任意のリソースグループを選択、もしくは新規作成します。
    • 場所:任意の場所を選択します。今回は、「東日本」を選択します。
    • Azure 実行アカウントの作成:「はい」を選択します。(詳細はこちら
  4. 入力が完了したら、「作成」を選択し、デプロイを行います。

  5. デプロイが無事完了しました。
    f:id:koruneko:20200719175301p:plain

モジュールを追加する

  1. 「共有リソース」より、「モジュール」を選択します。
    f:id:koruneko:20210204233452p:plain

  2. ギャラリーを参照を選択します。
    f:id:koruneko:20200719175739p:plain

  3. 検索窓より「AzureAD」と検索して、「AzureAD」を選択します。 f:id:koruneko:20210205005137p:plain

  4. インポートを行います。 f:id:koruneko:20210205005342p:plain f:id:koruneko:20210205005402p:plain

  5. モジュールが使用可能になりました。 f:id:koruneko:20210205013714p:plain

  6. 同様の手順で「MSOnline」も使用可能な状態にします。 f:id:koruneko:20210205015007p:plain

変数を利用する

共有リソースより、「変数」を選択します。

f:id:koruneko:20200722013829p:plain

「変数の追加」を選択して、変数の追加を行います。

f:id:koruneko:20200722014213p:plain

必要項目を入力します。
今回追加する変数は domain です。
自身のテナントのドメイン、"@xxx.co.jp"を設定します。

f:id:koruneko:20200722015132p:plain

  • 名前:任意の変数名を入力します。
  • 説明:変数の説明を記載します。
  • タイプ:変数の型をしていします。今回は「文字列」を指定します。
  • 値:変数の値を入力します。
  • 暗号化:暗号化を行うか選択します。

変数の設定が完了したら、PowerShell Runbook に戻ります。

先ほどのスクリプトに先ほど追加した変数の読み込みを追加します。

変数の読み込みは「Get-AutomationVariable -Name 'VariableName'」で行うことができます。

資格情報の追加

Microsoft 365 ユーザーの追加を行うことができるadmin のユーザー情報を記録します。

  1. 共有リソースより、「資格情報」を選択します。
    f:id:koruneko:20200719181415p:plain

  2. 「資格情報の追加」を選択します。
    f:id:koruneko:20200719181555p:plain

  3. 必要項目を入力します。
    f:id:koruneko:20200719181810p:plain

    • 名前:任意の名前を入力します。
    • 説明:後から確認したときにわかりやすいよう、対象の資格情報の説明を入力します。
    • ユーザー名:ユーザー名(上記説明の権限のあるユーザー)を入力します。
    • パスワード:パスワード(上記説明の権限のあるユーザーのパスワード)を入力します。
    • パスワードの確認入力:先ほど入力したパスワードをもう一度入力します。
  4. 「作成」を選択して完了です。

PowerShell スクリプトの登録

  1. プロセス オートメーションより「Runbook」を選択します。
    f:id:koruneko:20200719222338p:plain

  2. 「Runbook の作成」を選択します。
    f:id:koruneko:20200719222519p:plain

  3. 必要項目を入力します。
    f:id:koruneko:20200719222800p:plain

    • 名前:任意の名前を入力します。
    • Runbook の種類:「PowerShell」を選択します。
    • 説明:後から確認したときにわかりやすいよう、対象のRunbook の説明を入力します。
  4. 作成が完了するとPowerShell の編集画面が表示されます。
    f:id:koruneko:20200719223309p:plain

  5. Runbook には以下のように記載します。

Param
(
    [Parameter (Mandatory= $true)]
    $userRequest
)

# 変数の読込
$domain = Get-AutomationVariable -Name 'domain'

# 資格情報を読込
$myCredential = Get-AutomationPSCredential -Name 'Microsoft 365 admin Login'

# Microsoft 365 サブスクリプション用の Azure AD に接続する
Connect-MsolService -Credential $myCredential

# ユーザーの作成
$userRequest | ForEach-Object{
    $DisplayName = $_.DisplayName
    $FirstName = $_.FirstName
    $LastName = $_.LastName
    $UserPrincipalName = $_.UserPrincipalName + $domain

    New-MsolUser -DisplayName $DisplayName `
        -FirstName $FirstName `
        -LastName $LastName `
        -UserPrincipalName $UserPrincipalName `
        -UsageLocation JP `
        -LicenseAssignment xxx:O365_BUSINESS
}

送られてくる値はObject なので ForEach-Object でループ処理を行っています。

ユーザーの作成は、New-MsolUser で行っています。

使用可能なライセンス( -LicenseAssignment )は、Get-MsolAccountSku で確認を行ってください。

モジュールのインストールを行っていない場合は、PowerShell を使用して Microsoft 365 に接続する を参考にモジュールのインストールを行ってください。

なお、エラー処理は今回特に行っていないので、各自適宜行うようにしてください。

  1. テストを行う
    スクリプトの作成が完了したら、一度、テストを行っておきましょう。
    「テスト ウィンドウ」を選択して、パラメーターを入力後「開始」を選択します。

f:id:koruneko:20210208011752p:plain

f:id:koruneko:20210208011936p:plain

  1. 公開を行う
    作成が完了したら、「公開」を行ってください。
    これを行わないと、変更が反映されませんので、注意です。

f:id:koruneko:20210208012037p:plain

Power Automate

最後にPower Apps で入力された値を先ほど作成したPowerShell Runbook に渡すようにします。
また、このPower Automate ないで"承認"処理を行うようにします。

  1. Power Apps から呼び出されるので、トリガーには Power Apps を利用します。

f:id:koruneko:20210208012902p:plain

  1. Power Apps からはコレクションの内容をJSON で渡すようにするので、"JSON の解析"で内容を取得します。

f:id:koruneko:20210208013150p:plain

ちなみにスキーマには以下のように入力します。

{
    "type": "array",
    "items": {
        "type": "object",
        "properties": {
            "DisplayName": {
                "type": "string"
            },
            "FirstName": {
                "type": "string"
            },
            "ID": {
                "type": "integer"
            },
            "LastName": {
                "type": "string"
            },
            "UserPrincipalName": {
                "type": "string"
            }
        }
    }
}
  1. 承認者に申請内容が分かりやすいように申請内容を表示させたいと思います。
    以下のように申請内容の文字列を作成します。

f:id:koruneko:20210208013458p:plain

"値"は以下のように設定しています。

表示名:@{items('Apply_to_each')?['DisplayName']}@{uriComponentToString('%0A')}
姓:@{items('Apply_to_each')?['FirstName']}@{uriComponentToString('%0A')}
名:@{items('Apply_to_each')?['LastName']}@{uriComponentToString('%0A')}
アカウント名:@{items('Apply_to_each')?['UserPrincipalName']}@{uriComponentToString('%0A')}
@{uriComponentToString('%0A')}

uriComponentToString('%0A') は改行を表しています。

  1. 承認を作成します。
    詳細には先ほど作成した文字列を設定します。

f:id:koruneko:20210208013804p:plain

なおこの詳細メッセージはOutlook やPower Automate で表示したときは改行されて表示されますが、Teams で表示したときは改行して表示されないため注意が必要です。

  1. 承認の結果によって処理を分けたいと思います。

f:id:koruneko:20210208014118p:plain

承認された場合は、PowerShell Runbook を呼び出します。
却下された場合は、Teams で申請者にその旨を知らせます。

  1. 承認された場合はPowerShell Runbook を実行したいので"ジョブの作成"を作成します。

f:id:koruneko:20210208014404p:plain

  • サブスクリプション:Azure Automation 作成時に使用したサブスクリプションと同様のものを選択
  • リソースグループ:Azure Automation 作成時に使用したサブスクリプションと同様のものを選択
  • Automation アカウント:先ほど作成したAutomation アカウントを選択
  • Runbook 名:先ほど作成したPowerShell Runbook を選択
  • ジョブの待機:「はい」を選択
  • Runbook Parameter:作成したパラメーターがこちらに表示されます。今回は"JSONの解析_コンテンツ"を選択します。

なおこのコネクタはPremium コネクタのため注意してください。

  1. 却下された場合は申請者にTeams でメッセージを送りたいので"メッセージをフローボットとしてユーザーに投稿する"を作成します。

f:id:koruneko:20210208014849p:plain

わかりにくいですが、メッセージにはMarkdown が利用できるため、文の末尾にスペースを2つ入力しています。
これで改行になります。

〇応答コメント  
@{items('Apply_to_each_2')?['comments']}  

〇要求内容  
@{variables('request')}

Power Apps からPower Automate を呼び出す

最後にPower Apps からPower Automate を呼び出す処理を作成します。

"アクション"から"Power Automate"を選択して、先ほど作成したフローを追加します。

f:id:koruneko:20210208015413p:plain

Power Apps からPower Automate を呼び出すときには、

  • コレクションの内容をJSON にしたもの
  • 申請者のe-mail

を渡します。

Request ボタンの OnSelect に以下のように設定します。

RequestButton.OnSelect

Create_Microsoft365_User.Run(JSON(userRequest, JSONFormat.Compact), User().Email);
Navigate(Screen2)

以上でMicrosoft 365 ユーザー作成依頼を作成することができました!!

ここまで作成してみて、恐らく皆さんはPowerShell Runbook で受けた値の処理方法が適切ではない(エラーとなる)のでは?と思われているかと思います。
次の章で解説します。

Power Automate からPowerShell Runbook へJSON を送った場合の型について

Power Automate からPowerShell Runbook へJSON を送った場合、上記の場合、変数 $userRequest にはJSON が設定されるので、以下のように記載すべきかと思います。
(私も最初そう考えて処理を記載していましたが、 Invalid JSON エラーが表示されて詰まっていました...)

$userRequest | ConvertFrom-Json | ForEach-Object {
    $fields = $_
    
    $fields | ForEach-Object{
        $DisplayName = $_.DisplayName
        $FirstName = $_.FirstName
        $LastName = $_.LastName
        $UserPrincipalName = $_.UserPrincipalName + $domain

        New-MsolUser -DisplayName $DisplayName `
            -FirstName $FirstName `
            -LastName $LastName `
            -UserPrincipalName $UserPrincipalName `
            -UsageLocation JP `
            -LicenseAssignment xxx:O365_BUSINESS
    }
}

ただこの場合、上でも述べている通り、 ConvertFrom-JsonInvalid JSON エラーが表示されてしまいます。

Runbook の実行結果より、"入力"の値を確認したところ、値は確かにJSON で渡ってきており、念のためフォーマットも確認しましたが、正しい形式でした。

そこで、 echo などで実際に変数 $userRequest に設定されている値を確認した結果、どうやらオブジェクトとして変数には設定されているようでした。

なので上記のように設定しているというわけです。

何故オブジェクトになるのか???については現在調査中です。
もしわかる方や参考になりそうなdocs などありましたら教えていただけますと助かります。

---2021/02/23 追記---

作成されたユーザー情報を連携する

大事な処理を作成するのを忘れていました。。。
作成されたMicrosoft 365 ユーザーの情報を申請者へ連携しなくてはなりません。

ユーザー作成の結果はPowerShell Runbook の出力から確認することができます。

ただこの結果は確認していただければわかる通り、 System.Object[] 型で出力されています。

このままの結果をPower Automate に渡しても加工が難しいのでJSON に変換したいと思います。
JSON に変換するためには、 ConvertTo-Json を利用します。

Param
(
    [Parameter (Mandatory= $true)]
    $userRequest
)

$domain = Get-AutomationVariable -Name 'domain'

# 資格情報を読込
$myCredential = Get-AutomationPSCredential -Name 'Microsoft 365 admin Login'

# Microsoft 365 サブスクリプション用の Azure AD に接続する
Connect-MsolService -Credential $myCredential

# ユーザーの作成
$userRequest | ForEach-Object{
    $DisplayName = $_.DisplayName
    $FirstName = $_.FirstName
    $LastName = $_.LastName
    $UserPrincipalName = $_.UserPrincipalName + $domain

    New-MsolUser -DisplayName $DisplayName `
        -FirstName $FirstName `
        -LastName $LastName `
        -UserPrincipalName $UserPrincipalName `
        -UsageLocation JP `
        -LicenseAssignment korune:DEVELOPERPACK
} | ConvertTo-Json -Compress

これで結果がJSON で出力されるようになりました。

続いてPower Automate でのフローの編集です。

呼び出したPowerShell Runbook の出力結果を取得するには"ジョブの出力を取得します"というコネクタを利用する必要があります。

f:id:koruneko:20210223110131p:plain

その際、Runbook を呼び出した"ジョブの作成"コネクタの"ジョブの待機"は"はい"を選びましょう。

これでユーザーを作成した結果の情報をPower Automate でも扱えるようになったので、あとはデータを加工して必要な情報だけを申請者に共有するようにします。

まずはデータの加工です。
結果はJSON で送られてきているので、JSON の加工を行います。

f:id:koruneko:20210223111047p:plain

コンテンツ

@{body('ジョブの出力を取得します')?['body']}

スキーマ

{
    "type": "array",
    "items": {
        "type": "object",
        "properties": {
            "Password": {
                "type": "string"
            },
            "ExtensionData": {
                "type": "object",
                "properties": {}
            },
            "AlternateEmailAddresses": {
                "type": "array"
            },
            "AlternateMobilePhones": {
                "type": "array"
            },
            "AlternativeSecurityIds": {},
            "BlockCredential": {
                "type": "boolean"
            },
            "City": {},
            "CloudExchangeRecipientDisplayType": {},
            "Country": {},
            "Department": {},
            "DirSyncProvisioningErrors": {},
            "DisplayName": {
                "type": "string"
            },
            "Errors": {},
            "Fax": {},
            "FirstName": {
                "type": "string"
            },
            "ImmutableId": {},
            "IndirectLicenseErrors": {},
            "IsBlackberryUser": {
                "type": "boolean"
            },
            "IsLicensed": {
                "type": "boolean"
            },
            "LastDirSyncTime": {},
            "LastName": {
                "type": "string"
            },
            "LastPasswordChangeTimestamp": {
                "type": "string"
            },
            "LicenseAssignmentDetails": {
                "type": "array",
                "items": {
                    "type": "string"
                }
            },
            "LicenseReconciliationNeeded": {
                "type": "boolean"
            },
            "Licenses": {
                "type": "array",
                "items": {
                    "type": "string"
                }
            },
            "LiveId": {
                "type": "string"
            },
            "MSExchRecipientTypeDetails": {},
            "MSRtcSipDeploymentLocator": {},
            "MSRtcSipPrimaryUserAddress": {},
            "MobilePhone": {},
            "ObjectId": {
                "type": "string"
            },
            "Office": {},
            "OverallProvisioningStatus": {
                "type": "integer"
            },
            "PasswordNeverExpires": {
                "type": "boolean"
            },
            "PasswordResetNotRequiredDuringActivate": {},
            "PhoneNumber": {},
            "PortalSettings": {},
            "PostalCode": {},
            "PreferredDataLocation": {},
            "PreferredLanguage": {},
            "ProxyAddresses": {},
            "ReleaseTrack": {},
            "ServiceInformation": {},
            "SignInName": {
                "type": "string"
            },
            "SoftDeletionTimestamp": {},
            "State": {},
            "StreetAddress": {},
            "StrongAuthenticationMethods": {
                "type": "array"
            },
            "StrongAuthenticationPhoneAppDetails": {
                "type": "array"
            },
            "StrongAuthenticationProofupTime": {},
            "StrongAuthenticationRequirements": {
                "type": "array"
            },
            "StrongAuthenticationUserDetails": {},
            "StrongPasswordRequired": {
                "type": "boolean"
            },
            "StsRefreshTokensValidFrom": {
                "type": "string"
            },
            "Title": {},
            "UsageLocation": {
                "type": "string"
            },
            "UserLandingPageIdentifierForO365Shell": {},
            "UserPrincipalName": {
                "type": "string"
            },
            "UserThemeIdentifierForO365Shell": {},
            "UserType": {
                "type": "integer"
            },
            "ValidationStatus": {
                "type": "integer"
            },
            "WhenCreated": {}
        }
    }
}

上記の結果を申請者 / 管理者に送るためのメッセージに加工します。

f:id:koruneko:20210223111303p:plain

@{uriComponentToString('%0A')}-----------------------------------------------@{uriComponentToString('%0A')}
DisplayName:@{items('Apply_to_each_3')?['DisplayName']}@{uriComponentToString('%0A')}
FirstName:@{items('Apply_to_each_3')?['FirstName']}@{uriComponentToString('%0A')}
LastName:@{items('Apply_to_each_3')?['LastName']}@{uriComponentToString('%0A')}
UserPrincipalName:@{items('Apply_to_each_3')?['UserPrincipalName']}@{uriComponentToString('%0A')}
Password:@{items('Apply_to_each_3')?['Password']}@{uriComponentToString('%0A')}

@{uriComponentToString('%0A')} は改行です。

あとはこのメッセージをTeams で共有してあげれば完了です!

f:id:koruneko:20210223111428p:plain

Power Automate からPowerShell Runbook への値渡しについて

【PowerShell Runbook】 入力パラメータの型検証にて検証結果を纏めてありますのでもしよければ参考にしてください。

shibatea さんコメントなどでのアドバイスありがとうございました!

【Power Apps】パズルゲームを作成してみよう!

はじめに

今回は随分前に作成して動画もアップするとか宣言しておいてアップしていなかったPower Apps で作成したパズルゲームの作成方法について纏めようと思います。
動画は収録していたものがありましたが、今回改めて撮りなおしました。

2020年のやり残し(他多数)を2021年に行うことになるとは...

YouTube

動画での紹介はこちらになります。

youtu.be

よろしければ、 チャンネル登録、高評価お願いします!!

パズルゲームを作成する

ホーム画面を作成する

ホーム画面は単純で

  • ラベル
  • ボタン

の2つで構成されています。

まずはタイトルラベルを作成します。

ラベルを追加して画面上部に配置します。

Label.Text

"Puzzle Game"

Label.X

0

Label.Y

0

Label.Width

Parent.Width

Label.Height

200

Label.Color

RGBA(255, 255, 0, 1)

Label.Fill

RGBA(0, 121, 187, 1)

続いてゲーム開始を行うためのボタンを作成します。

このボタンには以下2つの役割があります。

  1. パズルの盤面を作成する
  2. ゲーム画面へ遷移する

1 の盤面を作成するために、ホーム画面が呼び出されたタイミングで盤面を初期化し、パズルの回答(どこを選択すればよいのか)を設定します。

上記を実現するために OnVisibel に以下を設定します。

Screen1.OnVisible

ClearCollect(
    originBoard,
    {No:1,  isFront:true},
    {No:2,  isFront:true},
    {No:3,  isFront:true},
    {No:4,  isFront:true},
    {No:5,  isFront:true},
    {No:6,  isFront:true},
    {No:7,  isFront:true},
    {No:8,  isFront:true},
    {No:9,  isFront:true},
    {No:10, isFront:true},
    {No:11, isFront:true},
    {No:12, isFront:true},
    {No:13, isFront:true},
    {No:14, isFront:true},
    {No:15, isFront:true},
    {No:16, isFront:true}
);
ClearCollect(
    answer,
    {No:1,  isSelect:Rand() > 0.5},
    {No:2,  isSelect:Rand() > 0.5},
    {No:3,  isSelect:Rand() > 0.5},
    {No:4,  isSelect:Rand() > 0.5},
    {No:5,  isSelect:Rand() > 0.5},
    {No:6,  isSelect:Rand() > 0.5},
    {No:7,  isSelect:Rand() > 0.5},
    {No:8,  isSelect:Rand() > 0.5},
    {No:9,  isSelect:Rand() > 0.5},
    {No:10, isSelect:Rand() > 0.5},
    {No:11, isSelect:Rand() > 0.5},
    {No:12, isSelect:Rand() > 0.5},
    {No:13, isSelect:Rand() > 0.5},
    {No:14, isSelect:Rand() > 0.5},
    {No:15, isSelect:Rand() > 0.5},
    {No:16, isSelect:Rand() > 0.5}
)

originBoard が問題の盤面です。
まずは初期化を行うのですべて true (表)としています。

answer がパズルの答えです。
1 / 2 の確率で対象のマスが選択すべきマスとして設定されることになります。

今回のパズルゲームのルールとして、選択したマスとそのマスに隣するすべてのマスの裏表が反転するというルールなので、 answer で作成されて答えをもとに、 originBoard のマスを作成しようと思います。

ボタンが選択されたときのアクションに以下を追加します。

Button.OnSelect

ForAll(
    RenameColumns(answer, "No", "ansNo"),
    If(
        isSelect,
        If(Mod(ansNo, 4) <> 1, UpdateIf(originBoard, No = ansNo - 5, {isFront:!LookUp(originBoard, No = ansNo - 5, isFront)}));
        UpdateIf(originBoard, No = ansNo - 4, {isFront:!LookUp(originBoard, No = ansNo - 4, isFront)});
        If(Mod(ansNo, 4) <> 0, UpdateIf(originBoard, No = ansNo - 3, {isFront:!LookUp(originBoard, No = ansNo - 3, isFront)}));
        If(Mod(ansNo, 4) <> 1, UpdateIf(originBoard, No = ansNo - 1, {isFront:!LookUp(originBoard, No = ansNo - 1, isFront)}));
        UpdateIf(originBoard, No = ansNo, {isFront:!isFront});
        If(Mod(ansNo, 4) <> 0, UpdateIf(originBoard, No = ansNo + 1, {isFront:!LookUp(originBoard, No = ansNo + 1, isFront)}));
        If(Mod(ansNo, 4) <> 1, UpdateIf(originBoard, No = ansNo + 3, {isFront:!LookUp(originBoard, No = ansNo + 3, isFront)}));
        UpdateIf(originBoard, No = ansNo + 4, {isFront:!LookUp(originBoard, No = ansNo + 4, isFront)});
        If(Mod(ansNo, 4) <> 0, UpdateIf(originBoard, No = ansNo + 5, {isFront:!LookUp(originBoard, No = ansNo + 5, isFront)}));
    )
);
Navigate(PlayScreen, ScreenTransition.Cover)

盤面の作成方法は単純で答えで選択するマスとして設定されたマスとそれに隣するマスの裏表を逆転させればいいですね。

盤面の作成が完了したので次はパズル画面の作成を行いたいと思います。

パズル画面を作成する

パズルの盤面を作成する

パズルの盤面を作成します。
盤面の作成にはギャラリーを利用します。

Items には先ほど作成した originBoard を利用したいですが、そのまま使用してしまうと"やり直し"ができなくなってしまいます。
なので originBoardplayBoard にコピーして、 playBoardItems に設定したいと思います。

コレクションの内容のコピーは以下のように行います。

Screen2.OnVisible

Clear(playBoard);
ForAll(
    originBoard,
    Collect(
        playBoard,
        {
            No:No,
            isFront:isFront
        }
    )
)

なおこの OnVisible では以下のように他変数も初期化しておいてください。
使用用途はその都度説明します。

Screen2.OnVisible

UpdateContext({_dispAns:false});
UpdateContext({_ansnum:1});
UpdateContext({_count:0});
UpdateContext({_isHint:false});
UpdateContext({_isRetire:false});
Clear(playBoard);
ForAll(
    originBoard,
    Collect(
        playBoard,
        {
            No:No,
            isFront:isFront
        }
    )
)

ギャラリーを設定して以下のように設定します。

Gallery.Items

playBoard

Gallery.X

(Parent.Width - Self.Width) / 2

Gallery.Y

(Parent.Height - Self.Height) / 2

Gallery.Width

Self.TemplateHeight * 4

Gallery.Height

Self.TemplateHeight * 4

Gallery.WrapCount

4

Gallery.TemplateSize

150

Gallery.TemplatePadding

0

ギャラリー内にラベルを配置し、以下のように設定します。

Label.Text

If(
    ThisItem.isFront,
    "〇",
    "×"
)

Label.Color

If(
    ThisItem.isFront,
    RGBA(255, 0, 0, 1),
    RGBA(0, 0, 255, 1)
)

Label.Fill

If(
    ThisItem.isFront,
    Pink,
    Aqua
)

Label.Size

80

このラベル(マス)が選択されたとき、そのマスとそれに隣するマス全ての裏表を逆転させたいので以下のように設定します。

Label.OnSelect

If(Mod(ThisItem.No, 4) <> 1, UpdateIf(playBoard, No = ThisItem.No - 5, {isFront:!LookUp(playBoard, No = ThisItem.No - 5, isFront)}));
UpdateIf(playBoard, No = ThisItem.No - 4, {isFront:!LookUp(playBoard, No = ThisItem.No - 4, isFront)});
If(Mod(ThisItem.No, 4) <> 0, UpdateIf(playBoard, No = ThisItem.No - 3, {isFront:!LookUp(playBoard, No = ThisItem.No - 3, isFront)}));
If(Mod(ThisItem.No, 4) <> 1, UpdateIf(playBoard, No = ThisItem.No - 1, {isFront:!LookUp(playBoard, No = ThisItem.No - 1, isFront)}));
UpdateIf(playBoard, No = ThisItem.No, {isFront:!ThisItem.isFront});
If(Mod(ThisItem.No, 4) <> 0, UpdateIf(playBoard, No = ThisItem.No + 1, {isFront:!LookUp(playBoard, No = ThisItem.No + 1, isFront)}));
If(Mod(ThisItem.No, 4) <> 1, UpdateIf(playBoard, No = ThisItem.No + 3, {isFront:!LookUp(playBoard, No = ThisItem.No + 3, isFront)}));
UpdateIf(playBoard, No = ThisItem.No + 4, {isFront:!LookUp(playBoard, No = ThisItem.No + 4, isFront)});
If(Mod(ThisItem.No, 4) <> 0, UpdateIf(playBoard, No = ThisItem.No + 5, {isFront:!LookUp(playBoard, No = ThisItem.No + 5, isFront)}));
UpdateContext({_count:_count + 1})

UpdateContext({_count:_count + 1}) では、ユーザーの手数をカウントしています。
OnVisble で初期化を行っていますね。

以上で盤面の作成は完了です。

経過ターンを表示する

経過ターンを表示するためのラベルを作成します。

ラベルを配置して以下のように設定します。

Label.Text

_count & "ターン経過" & If(_isHint, "(最短" & CountRows(Filter(answer, isSelect = true)) & "回)")

Label.Width

Parent.Width

Label.Height

Gallery.Y

If(_isHint, "(最短" & CountRows(Filter(answer, isSelect = true)) & "回)") は後に作成するヒント機能の実装部分になります。

ホームボタンを作成する

ホーム画面に戻るボタンを作成します。

家のアイコンを追加して以下のように設定します。

HomeIcon.OnSelect

Back()

今回画面は2画面だけで、この画面はホーム画面からのみ遷移されてくるので、 Back() で戻ることとします。

リセットボタンを作成する

リセットボタンには OnVisible と同じ内容を設定すればいいですね。

再読み込みのアイコンを設定して以下のように設定します。

ResetIcon.OnSelect

UpdateContext({_dispAns:false});
UpdateContext({_ansnum:1});
UpdateContext({_count:0});
UpdateContext({_isHint:false});
UpdateContext({_isRetire:false});
Clear(playBoard);
ForAll(
    originBoard,
    Collect(
        playBoard,
        {
            No:No,
            isFront:isFront
        }
    )
)

ResetIcon.Y

HomeIcon.Y + HomeIcon.Height + 10

ヒントボタンを作成する

ヒントボタンは、押されると最短手数を表示するようにします。
先ほど作成した経過ターンのラベルですね。

isHinttrue に設定すれば、表示されるよう先ほど設定していたのでそのように設定します。

星のアイコンを設定して以下のように設定します。

HintIcon.OnSelect

UpdateContext({_isHint:true});

HintIcon.Y

ResetIcon.Y + ResetIcon.Height + 10

答えを表示する

答えを表示するためのボタンを設定して以下のように設定します。

Button.OnSelect

UpdateContext({_count:0});
UpdateContext({_isRetire:true});
Clear(playBoard);
ForAll(
    originBoard, 
    Collect(
        playBoard,
        {
            No:No,
            isFront:isFront
        }
    )
);
UpdateContext({_dispAns:true});

答えを表示するために、一度カウントと盤面をリセットしています。
また、答え表示のためのフラグ _dispAns とユーザーが自力で解けずリタイアしたことを表すためのフラグ _isRetiretrue に設定しています。

答え表示のためのフラグ _dispAnstrue に設定されたことをトリガーにユーザーに答えを示したいと思います。

答えの表示処理にはタイマーを利用します。

タイマーを設定して以下のように設定します。

Timer.Duration

1000

Timer.OnTimerEnd

If(
    CountRows(Filter(answer, isSelect = true)) >= _ansnum,
    UpdateContext({_dispAns:true});
    Select(BoardGallery, Last(FirstN(Filter(answer, isSelect = true), _ansnum)).No, Panel),
    UpdateContext({_dispAns:false})
);
UpdateContext({_ansnum:_ansnum + 1});

TimerAutoStart

CountRows(Filter(answer, isSelect = true)) >= _ansnum && _dispAns

Timer.Repeat

CountRows(Filter(answer, isSelect = true)) >= _ansnum && _dispAns

タイマーは1000ms 間隔で繰り返し処理を行うようにします。

タイマーの処理終了時、 _ansnum の値が answertrue の数以下(選択すべきマス以下)であった場合、 _ansnum 番目の選択すべきマスを選択しています。
_ansnum の値が answertrue の数以上であった場合は、 _dispAnsfalse に設定しています。

答えの表示が全て終了したらループを止めたいので、リピート処理を上記のように設定しています。

このままでは、ユーザーがどのマスを選択すればよかったのか?視覚的にわかりにくいので、答えの表示時に選択されたマスを赤丸で示したいと思います。

ギャラリー内にサークルを追加して、以下のように設定します。

Circle.X

(Panel.Width - Self.Width) / 2

Circle.Y

(Panel.Height - Self.Height) / 2

Circle.Fill

RGBA(255, 0, 0, 1)

Circle.OnVisible

ThisItem.No = Last(FirstN(Filter(answer, isSelect = true), _ansnum - 1)).No && _dispAns && CountRows(Filter(answer, isSelect = true)) >= _ansnum - 1

(Panel は Label です。)

これで答えの表示を実装することができました!

クリア画面を作成する

最後にクリア画面を作成したいと思います。

このクリア画面は、答え表示で全てのパネルが表になったときではなく、ユーザーが自身で選択したことによって全てのマスが表になったときに表示するようにします。

Label.Text

"Clear!!"

Label.OnVisible

CountIf(playBoard, isFront = true) = CountRows(playBoard) && !_isRetire

Label.Size

250

最後にホーム画面へ戻るアイコンたちを最前面にして完成です!
これをし忘れると、クリアラベルが表示されたとき、アイコンたちより上にいた場合アイコンが選択できなくて画面を操作できなくなって詰みます。

おわりに

以上でパズルアプリの作成方法は終了です!

最近はSVG を利用したアプリが多めでしたが、こんな風にPower Apps の標準的?な機能だけで簡単にゲームが作成できてしまうので良い時代になりましたね。 Power Apps はそういった用途のアプリじゃないですが‼

【Power Apps】タブ遷移に関して

はじめに

今回の記事はTips てきな記事になります。

Power Apps ではアクセシビリティを向上させるためのTabIndex プロパティが存在します。
この設定を行うことにより、ユーザーがキーボードだけで操作を行うことができるようになります。

今回はこのTabIndex プロパティについて纏めようと思います。

TabIndex プロパティを設定してみる

TabIndex プロパティは各コントロールに設定することが可能です。

TabIndex プロパティは外面上部にある数式バーか、画面右にあるコントロールのプロパティから変更が可能です。

f:id:koruneko:20210103004157p:plain

f:id:koruneko:20210103003900p:plain

日本語名称では、"タブ遷移順"となっていますが、ここに設定する数値は0 or -1が推奨されています。
それぞれ以下の動作となります。

TabIndex 値 動作 既定
0 コントロールはキーボード ナビゲーションに関与します。 Button、Text input、Combo box、およびその他の一般的な対話型コントロール
-1 コントロールはキーボード ナビゲーションに関与できません。 Label、Image、Icon、およびその他の一般的な非対話型コントロール

簡単に説明すると、
0を設定するとタブ遷移を行い -1を設定するとタブ遷移を行いません。

このタブ遷移は特に特別な設定を行っていなかった場合、Z順で遷移を行います。

詳しくはこちらをご確認ください。

docs.microsoft.com

デフォルト値が-1のコントロールに関しても、もちろん0に設定することでタブ遷移を行えるようにすることが可能です。
その場合、 FocusedBorderColor に任意の色を設定し、 FocusedBorderThickness に0より大きい値を設定してください。
この設定を行わないと、現在フォーカス設定されているコントロールがユーザーからわからなくなってしまいます。

TabIndex のデフォルト値が0のコントトールは上記プロパティに自身のボーダーカラーと線の太さが0より大きい値が設定されていますのでフォーカスされているコントロールがわかるようになっています。

実際の動作

まずは以下のようにテキストボックスを5つ配置したときのタブ遷移です。

ヒントのテキストに数値が設定されているテキストボックスの TabIndex には0が設定されており、×が設定されている TabIndex には-1が設定されています。

続いてギャラリーのタブ遷移です。

ギャラリーは WrapCount で折り返しの数を指定することが可能です。
この数値を設定することで1行(1列)あたりに表示されるコントロールの数を指定できます。

今回は WrapCount に3を設定しているので3行ずつ表示されています。
この状態でタブ遷移を行うと以下のようになります。

上記のようにZ順でテキストボックスにタブ遷移が行われます。
ラベルには TabIndex に-1を設定しているのでタブ遷移が行われません。

続いてギャラリー内でタブ遷移を行ったときに、ギャラリー外から選択されている項目を参照するとどのような動作になるのかみてみましょう!

上記のようにタブ遷移でユーザーがコントロールへ移動した場合、外部からはユーザーが現在どのコントロールを選択しているのかわかりません。
これは恐らくタブ遷移ではコントロールを選択するのではなく、あくまでフォーカスを行っているだけだからかと思われます。

もし、ギャラリー内でユーザーが選択している項目によってなにか変化させるような実装を行いたい場合は、このような制限事項があるので注意が必要です。
現時点(2021/1/2)ではこの問題を回避するベストな方法が存在しないので、ケースバイケースで対応もしくは許容する必要があります。

最後にタブ遷移をZ順ではなくN順で行わせる方法についてです。

まずは下記のように、 TabIndex のみを設定した場合は以下のようにZ順でタブ遷移を行います。

上記のように上から下へ、左から右にタブ遷移を行っていますね。

これを、「テキストボックス → トグル」といった風にN順でタブ遷移を行うように設定してみます。
上記を実現するためにはグループをコンテナー内に設定します。

これにより、上記のようにN順でタブ遷移が行われるようになりましたね。

1点注意として、N順でタブ遷移をしたい場合は、グループ化ではなくコンテナ化する必要があるという点です。
グループ化とした場合は以下のようにZ順でタブ遷移を行います。

これらの違いに気を付けて各コントロールを設定しましょう。

【Power Apps】地理クイズアプリを作成してみよう!

はじめに

Power Apps で地理クイズを行える学習アプリを作成してみましたので、作成方法を纏めたいと思います。

お子様をお持ちのご家庭でかつHome 365 な誤家庭ご家庭の方は是非お子さんと一緒に作成してみて、アプリの作成と地理の学習という英才教育に役立ててください!

参考

今回利用したSVG 素材は以下からお借りしました。

github.com

アプリをダウンロードする場合

こちらに雑においてます。

.maspp ファイルをダウンロードしてください。

GitHub - koruneko/GeographyQize: for Power Apps

地理クイズアプリを作成する

日本地図を表示させる

まずは、下図のような日本地図を表示させたいと思います。

f:id:koruneko:20210101023332p:plain

画像コントロールを追加して以下のように設定します。

Screen.OnVisible

UpdateContext({scoring:false});
UpdateContext({nowSelect:0});
ClearCollect(
    Prefectures,
    {Num:1, Pref:"北海道", PrefCap:"札幌市", PrefAns:"", PrefCapAns:""},
    {Num:2, Pref:"青森県", PrefCap:"青森市", PrefAns:"", PrefCapAns:""},
    {Num:3, Pref:"岩手県", PrefCap:"盛岡市", PrefAns:"", PrefCapAns:""},
    {Num:4, Pref:"宮城県", PrefCap:"仙台市", PrefAns:"", PrefCapAns:""},
    {Num:5, Pref:"秋田県", PrefCap:"秋田市", PrefAns:"", PrefCapAns:""},
    {Num:6, Pref:"山形県", PrefCap:"山形市", PrefAns:"", PrefCapAns:""},
    {Num:7, Pref:"福島県", PrefCap:"福島市", PrefAns:"", PrefCapAns:""},
    {Num:8, Pref:"茨城県", PrefCap:"水戸市", PrefAns:"", PrefCapAns:""},
    {Num:9, Pref:"栃木県", PrefCap:"宇都宮市", PrefAns:"", PrefCapAns:""},
    {Num:10, Pref:"群馬県", PrefCap:"前橋市", PrefAns:"", PrefCapAns:""},
    {Num:11, Pref:"埼玉県", PrefCap:"さいたま市", PrefAns:"", PrefCapAns:""},
    {Num:12, Pref:"千葉県", PrefCap:"千葉市", PrefAns:"", PrefCapAns:""},
    {Num:13, Pref:"東京都", PrefCap:"新宿区", PrefAns:"", PrefCapAns:""},
    {Num:14, Pref:"神奈川県", PrefCap:"横浜市", PrefAns:"", PrefCapAns:""},
    {Num:15, Pref:"新潟県", PrefCap:"新潟市", PrefAns:"", PrefCapAns:""},
    {Num:16, Pref:"富山県", PrefCap:"富山市", PrefAns:"", PrefCapAns:""},
    {Num:17, Pref:"石川県", PrefCap:"金沢市", PrefAns:"", PrefCapAns:""},
    {Num:18, Pref:"福井県", PrefCap:"福井市", PrefAns:"", PrefCapAns:""},
    {Num:19, Pref:"山梨県", PrefCap:"甲府市", PrefAns:"", PrefCapAns:""},
    {Num:20, Pref:"長野県", PrefCap:"長野市", PrefAns:"", PrefCapAns:""},
    {Num:21, Pref:"岐阜県", PrefCap:"岐阜市", PrefAns:"", PrefCapAns:""},
    {Num:22, Pref:"静岡県", PrefCap:"静岡市", PrefAns:"", PrefCapAns:""},
    {Num:23, Pref:"愛知県", PrefCap:"名古屋市", PrefAns:"", PrefCapAns:""},
    {Num:24, Pref:"三重県", PrefCap:"津市", PrefAns:"", PrefCapAns:""},
    {Num:25, Pref:"滋賀県", PrefCap:"大津市", PrefAns:"", PrefCapAns:""},
    {Num:26, Pref:"京都府", PrefCap:"京都市", PrefAns:"", PrefCapAns:""},
    {Num:27, Pref:"大阪府", PrefCap:"大阪市", PrefAns:"", PrefCapAns:""},
    {Num:28, Pref:"兵庫県", PrefCap:"神戸市", PrefAns:"", PrefCapAns:""},
    {Num:29, Pref:"奈良県", PrefCap:"奈良市", PrefAns:"", PrefCapAns:""},
    {Num:30, Pref:"和歌山県", PrefCap:"和歌山市", PrefAns:"", PrefCapAns:""},
    {Num:31, Pref:"鳥取県", PrefCap:"鳥取市", PrefAns:"", PrefCapAns:""},
    {Num:32, Pref:"島根県", PrefCap:"松江市", PrefAns:"", PrefCapAns:""},
    {Num:33, Pref:"岡山県", PrefCap:"岡山市", PrefAns:"", PrefCapAns:""},
    {Num:34, Pref:"広島県", PrefCap:"広島市", PrefAns:"", PrefCapAns:""},
    {Num:35, Pref:"山口県", PrefCap:"山口市", PrefAns:"", PrefCapAns:""},
    {Num:36, Pref:"徳島県", PrefCap:"徳島市", PrefAns:"", PrefCapAns:""},
    {Num:37, Pref:"香川県", PrefCap:"高松市", PrefAns:"", PrefCapAns:""},
    {Num:38, Pref:"愛媛県", PrefCap:"松山市", PrefAns:"", PrefCapAns:""},
    {Num:39, Pref:"高知県", PrefCap:"高知市", PrefAns:"", PrefCapAns:""},
    {Num:40, Pref:"福岡県", PrefCap:"福岡市", PrefAns:"", PrefCapAns:""},
    {Num:41, Pref:"佐賀県", PrefCap:"佐賀市", PrefAns:"", PrefCapAns:""},
    {Num:42, Pref:"長崎県", PrefCap:"長崎市", PrefAns:"", PrefCapAns:""},
    {Num:43, Pref:"熊本県", PrefCap:"熊本市", PrefAns:"", PrefCapAns:""},
    {Num:44, Pref:"大分県", PrefCap:"大分市", PrefAns:"", PrefCapAns:""},
    {Num:45, Pref:"宮崎県", PrefCap:"宮崎市", PrefAns:"", PrefCapAns:""},
    {Num:46, Pref:"鹿児島県", PrefCap:"鹿児島市", PrefAns:"", PrefCapAns:""},
    {Num:47, Pref:"沖縄県", PrefCap:"那覇市", PrefAns:"", PrefCapAns:""}
)

Image.Image

長いので折りたたんで表示しています。 "data:image/svg+xml,"& EncodeUrl( "<svg viewBox='-700 0 1700 1000' xmlns='http://www.w3.org/2000/svg'> <title>Japanese Prefectures</title> <desc>Created by Geolonia (https://geolonia.com/).</desc> <g class='svg-map' transform='matrix(1.028807, 0, 0, 1.028807, -47.544239, -28.806583)'> <g class='prefectures' transform='matrix(1, 0, 0, 1, 6, 18)'> <g class='okinawa kyusyu-okinawa prefecture' data-code='47' stroke-linejoin='round' fill='" & If(nowSelect = 47, "#FFFF00", If(LookUp(Prefectures, Num = 47, PrefAns) <> "", "#FF0000", "#EEEEEE")) & "' fill-rule='nonzero' stroke='#000000' stroke-width='1.0' transform='translate(52.000000, 193.000000)'> <title>沖縄 / Okinawa</title> <polygon points='4 109 6 110 4 111 0 110'/> <polygon points='48 121 55 123 51 129 39 124 42 122 44 125 46 118'/> <polygon points='132 113 130 110 132 110 130 108 133 105 132 100 135 108 142 114'/> <polygon points='225 23 224 28 219 23 223 21'/> <polygon points='73 117 77 112 79 113 73 120 72 126 66 127 64 125 67 122 63 119 71 120'/> <polygon points='287 20 291 17 286 15 285 8 292 10 292 14 299 13 298 11 307 0 309 5 309 9 305 15 300 15 299 19 293 19 294 21 288 25 281 25 286 34 281 31 276 39 280 41 270 46 270 38 277 32 275 24 280 25'/> <polygon points='127 106 126 103 128 104'/> <polygon points='279 8 279 6 283 8'/> <polygon points='293 11 294 13 292 12'/> </g> <g class='kagoshima kyusyu kyusyu-okinawa prefecture' data-code='46' stroke-linejoin='round' fill='" & If(nowSelect = 46, "#FFFF00", If(LookUp(Prefectures, Num = 46, PrefAns) <> "", "#FF0000", "#EEEEEE")) & "' fill-rule='nonzero' stroke='#000000' stroke-width='1.0' transform='translate(96.000000, 17.000000)'> <title>鹿児島 / Kagoshima</title> <polygon points='23 949 26 951 23 952'/> <polygon points='33 960 32 954 39 950 48 956 47 961 41 965 35 964'/> <polygon points='64 953 64 958 59 959 58 952 62 949 63 939 70 929 71 938'/> <polygon points='38 844 43 848 52 844 57 850 56 852 61 859 66 864 65 869 70 871 73 880 80 881 80 888 78 891 75 890 70 896 75 899 73 903 76 902 69 905 66 910 50 918 50 913 55 910 59 898 53 889 54 884 48 881 53 879 54 883 57 884 61 878 54 872 50 874 44 887 46 898 51 901 49 906 43 907 40 901 27 901 28 897 25 896 27 894 22 890 31 890 35 880 32 871 27 867 30 860 28 848 31 846 35 848'/> <polygon points='31 837 34 836 33 838'/> <polygon points='27 848 24 842 29 840 30 844'/> <polygon points='4 868 6 865 7 867 2 875 0 874'/> <polygon points='12 864 9 861 14 861'/> <polygon points='284 149 280 150 279 146 289 144'/> <polygon points='301 126 301 118 306 118 305 122 309 126 308 129 303 132 300 128'/> <polygon points='363 98 360 99 359 97 365 93'/> <polygon points='344 90 331 98 335 101 330 104 327 103 329 107 323 102 325 99 316 97 324 97 325 95 321 94 335 88 337 90 339 86 344 85 342 89 344 87 345 89 347 82 349 88'/> <polygon points='324 108 322 106 320 108 318 101 323 102 322 105 328 107 325 109 326 107'/> <polygon points='355 12 352 16 352 13'/> <polygon points='361 1 363 0 365 4'/> </g> <g class='miyazaki kyusyu kyusyu-okinawa prefecture' data-code='45' stroke-linejoin='round' fill='" & If(nowSelect = 45, "#FFFF00", If(LookUp(Prefectures, Num = 45, PrefAns) <> "", "#FF0000", "#EEEEEE")) & "' fill-rule='nonzero' stroke='#000000' stroke-width='1.0' transform='translate(152.000000, 824.000000)'> <title>宮崎 / Miyazaki</title> <polygon points='36 0 38 1 43 0 48 5 56 5 59 1 63 2 63 7 65 7 54 17 53 21 56 22 52 23 51 25 53 26 47 34 41 51 39 74 34 79 32 91 27 89 22 84 24 81 24 74 17 73 14 64 9 62 10 57 5 52 0 45 1 43 22 41 19 35 23 30 18 23 18 16 23 14 32 0'/> </g> <g class='oita kyusyu kyusyu-okinawa prefecture' data-code='44' stroke-linejoin='round' fill='" & If(nowSelect = 44, "#FFFF00", If(LookUp(Prefectures, Num = 44, PrefAns) <> "", "#FF0000", "#EEEEEE")) & "' fill-rule='nonzero' stroke='#000000' stroke-width='1.0' transform='translate(163.000000, 771.000000)'> <title>大分 / Oita</title> <polygon points='0 34 3 29 0 26 2 24 1 19 5 13 12 9 19 10 20 3 33 7 38 0 47 4 49 10 46 18 43 17 43 20 35 22 36 26 40 28 56 27 50 36 56 36 53 38 56 40 61 38 62 41 57 41 55 45 65 49 59 49 61 51 57 54 60 55 54 56 54 60 52 60 52 55 48 54 45 58 37 58 32 53 27 54 25 53 21 49 22 43 15 30 9 30 10 36 8 39'/> </g> <g class='kumamoto kyusyu kyusyu-okinawa prefecture' data-code='43' stroke-linejoin='round' fill='" & If(nowSelect = 43, "#FFFF00", If(LookUp(Prefectures, Num = 43, PrefAns) <> "", "#FF0000", "#EEEEEE")) & "' fill-rule='nonzero' stroke='#000000' stroke-width='1.0' transform='translate(115.000000, 800.000000)'> <title>熊本 / kumamoto</title> <path d='M8,37 L13,38 L13,48 L5,57 L1,58 L1,52 L5,51 L0,50 L4,40 L3,37 L8,37 Z M20,47 L14,47 L12,43 L19,39 L26,39 L23,47 L20,47 Z M24,34 L26,34 L26,38 L23,38 L24,34 Z M38,67 L33,61 L24,65 L19,61 L32,46 L30,40 L32,41 L31,39 L37,34 L26,34 L34,28 L35,23 L27,15 L26,10 L31,10 L30,7 L35,3 L39,4 L40,0 L48,5 L56,10 L58,7 L57,1 L63,1 L70,14 L69,20 L73,24 L69,24 L60,38 L55,40 L55,47 L60,54 L56,59 L59,65 L38,67 Z'/> </g> <g class='nagasaki kyusyu kyusyu-okinawa prefecture' data-code='42' stroke-linejoin='round' fill='" & If(nowSelect = 42, "#FFFF00", If(LookUp(Prefectures, Num = 42, PrefAns) <> "", "#FF0000", "#EEEEEE")) & "' fill-rule='nonzero' stroke='#000000' stroke-width='1.0' transform='translate(44.000000, 700.000000)'> <title>長崎 / Nagasaki</title> <path d='M53,1 L55,0 L57,2 L54,5 L55,9 L49,15 L48,19 L51,18 L49,22 L51,21 L48,25 L48,20 L46,22 L46,19 L45,22 L42,20 L44,20 L45,15 L47,15 L45,14 L49,10 L46,8 L48,3 L52,3 L53,1 Z M46,29 L43,35 L37,36 L40,22 L41,24 L45,25 L44,22 L45,26 L48,25 L46,29 Z M67,59 L68,61 L64,63 L60,59 L62,59 L61,56 L64,53 L67,55 L66,57 L68,58 L67,59 Z M28,110 L25,115 L26,110 L23,107 L26,106 L26,102 L28,102 L30,94 L28,105 L33,106 L30,108 L28,106 L28,110 Z M24,112 L21,111 L21,109 L23,111 L22,108 L24,112 Z M19,113 L20,110 L19,116 L16,111 L19,113 Z M6,118 L9,117 L10,119 L12,115 L16,125 L9,124 L9,129 L0,126 L1,124 L3,126 L4,125 L2,125 L5,122 L4,116 L6,118 Z M65,80 L62,81 L64,78 L65,80 Z M67,82 L70,84 L67,86 L67,82 Z M29,88 L31,85 L33,87 L29,88 Z M54,76 L51,77 L55,74 L54,76 Z M66,86 L64,89 L66,96 L73,98 L72,102 L78,109 L86,112 L79,117 L92,118 L94,123 L92,130 L82,134 L80,128 L85,124 L84,122 L79,121 L71,123 L68,129 L60,134 L64,129 L63,126 L65,127 L67,123 L64,125 L63,118 L59,117 L56,111 L59,100 L63,104 L62,108 L63,106 L66,108 L64,115 L75,118 L71,112 L72,107 L69,103 L66,105 L65,99 L62,99 L62,97 L58,100 L60,98 L57,96 L57,93 L57,95 L53,93 L57,87 L54,86 L55,83 L60,82 L60,85 L65,84 L66,86 Z M49,88 L48,92 L42,94 L47,91 L45,89 L48,84 L52,84 L54,81 L55,84 L49,88 Z M15,115 L15,113 L18,116 L15,118 L15,115 Z M64,101 L63,104 L62,101 L64,98 L64,101 Z'/> </g> <g class='saga kyusyu kyusyu-okinawa prefecture' data-code='41' stroke-linejoin='round' fill='" & If(nowSelect = 41, "#FFFF00", If(LookUp(Prefectures, Num = 41, PrefAns) <> "", "#FF0000", "#EEEEEE")) & "' fill-rule='nonzero' stroke='#000000' stroke-width='1.0' transform='translate(108.000000, 773.000000)'> <title>佐賀 / Saga</title> <polygon points='15 6 28 6 34 12 41 10 40 16 32 21 30 28 25 23 19 29 22 39 14 36 8 29 9 25 2 23 0 16 2 13 5 17 4 13 6 9 2 6 3 4 6 6 6 0 7 2 11 2 12 7'/> </g> <g class='fukuoka kyusyu kyusyu-okinawa prefecture' data-code='40' stroke-linejoin='round' fill='" & If(nowSelect = 40, "#FFFF00", If(LookUp(Prefectures, Num = 40, PrefAns) <> "", "#FF0000", "#EEEEEE")) & "' fill-rule='nonzero' stroke='#000000' stroke-width='1.0' transform='translate(123.000000, 752.000000)'> <title>福岡 / Fukuoka</title> <polygon points='40 53 32 48 31 52 27 51 22 55 23 58 18 58 18 52 15 49 17 42 25 37 26 31 19 33 13 27 0 27 7 23 3 20 10 15 13 21 20 20 21 15 17 17 14 14 19 15 23 12 25 5 35 3 36 0 42 1 38 5 41 5 43 2 46 4 53 0 49 8 52 10 50 11 55 22 60 22 59 29 52 28 45 32 41 38 42 43 40 45 43 48'/> </g> <g class='kochi shikoku prefecture' data-code='39' stroke-linejoin='round' fill='" & If(nowSelect = 39, "#FFFF00", If(LookUp(Prefectures, Num = 39, PrefAns) <> "", "#FF0000", "#EEEEEE")) & "' fill-rule='nonzero' stroke='#000000' stroke-width='1.0' transform='translate(251.000000, 765.000000)'> <title>高知 / Kochi</title> <path d='M61,0 L74,6 L80,4 L81,12 L88,13 L86,17 L88,21 L94,22 L87,41 L75,25 L65,21 L55,23 L55,21 L56,24 L44,28 L50,28 L44,29 L42,32 L41,29 L38,33 L39,38 L37,44 L34,44 L30,52 L25,53 L25,61 L22,64 L25,70 L21,66 L14,68 L9,65 L5,67 L6,61 L10,60 L9,57 L6,57 L8,54 L5,41 L9,44 L15,36 L20,33 L16,24 L27,23 L30,13 L37,4 L61,0 Z M1,69 L0,70 L0,67 L1,69 Z'/> </g> <g class='ehime sikoku prefecture' data-code='38' stroke-linejoin='round' fill='" & If(nowSelect = 38, "#FFFF00", If(LookUp(Prefectures, Num = 38, PrefAns) <> "", "#FF0000", "#EEEEEE")) & "' fill-rule='nonzero' stroke='#000000' stroke-width='1.0' transform='translate(225.000000, 737.000000)'> <title>愛媛 / Ehime</title> <path d='M34,21 L31,20 L35,17 L34,21 Z M55,6 L51,6 L55,0 L57,3 L55,6 Z M60,5 L61,5 L60,7 L57,6 L60,5 Z M58,10 L54,12 L55,7 L60,8 L58,10 Z M32,85 L25,83 L25,86 L23,86 L24,81 L27,82 L21,76 L24,76 L22,72 L25,72 L22,71 L23,69 L20,67 L24,70 L28,66 L28,63 L24,63 L27,60 L19,60 L21,57 L19,56 L22,51 L16,50 L8,56 L0,57 L35,37 L41,18 L50,15 L48,11 L51,10 L59,24 L70,20 L80,22 L84,18 L89,20 L87,28 L63,32 L56,41 L53,51 L42,52 L46,61 L41,64 L35,72 L31,69 L34,82 L32,85 Z'/> </g> <g class='kagawa shikoku prefecture' data-code='37' stroke-linejoin='round' fill='" & If(nowSelect = 37, "#FFFF00", If(LookUp(Prefectures, Num = 37, PrefAns) <> "", "#FF0000", "#EEEEEE")) & "' fill-rule='nonzero' stroke='#000000' stroke-width='1.0' transform='translate(308.000000, 724.000000)'> <title>香川 / Kagawa</title> <path d='M6,33 L1,31 L4,21 L0,17 L6,20 L19,10 L26,13 L30,9 L31,14 L34,11 L37,14 L36,17 L45,22 L44,25 L31,23 L22,30 L15,27 L6,33 Z M27,5 L25,4 L29,4 L27,5 Z M40,0 L42,0 L40,8 L38,7 L38,5 L35,9 L35,5 L31,3 L40,0 Z'/> </g> <g class='tokushima shikoku prefecture' data-code='36' stroke-linejoin='round' fill='" & If(nowSelect = 36, "#FFFF00", If(LookUp(Prefectures, Num = 36, PrefAns) <> "", "#FF0000", "#EEEEEE")) & "' fill-rule='nonzero' stroke='#000000' stroke-width='1.0' transform='translate(312.000000, 744.000000)'> <title>徳島 / Tokushima</title> <path d='M50,2 L52,0 L51,4 L50,2 Z M41,2 L49,0 L49,3 L52,4 L48,13 L55,20 L50,24 L57,26 L38,36 L33,43 L27,42 L25,38 L27,34 L20,33 L19,25 L13,27 L0,21 L2,13 L11,7 L18,10 L27,3 L40,5 L41,2 Z'/> </g> <g class='yamaguchi chugoku prefecture' data-code='35' stroke-linejoin='round' fill='" & If(nowSelect = 35, "#FFFF00", If(LookUp(Prefectures, Num = 35, PrefAns) <> "", "#FF0000", "#EEEEEE")) & "' fill-rule='nonzero' stroke='#000000' stroke-width='1.0' transform='translate(168.000000, 710.000000)'> <title>山口 / Yamaguchi</title> <path d='M45,0 L47,6 L43,11 L45,15 L50,15 L48,20 L50,24 L56,22 L58,24 L61,20 L60,16 L64,14 L64,21 L66,23 L67,29 L72,31 L72,36 L69,36 L70,44 L65,46 L65,54 L61,48 L57,48 L52,42 L47,44 L50,41 L46,38 L35,42 L32,39 L30,42 L29,40 L27,42 L27,38 L25,43 L21,45 L19,43 L16,45 L15,40 L9,36 L3,43 L3,36 L0,32 L4,27 L1,21 L4,17 L10,17 L5,15 L7,12 L16,14 L17,17 L24,17 L30,15 L29,12 L41,1 L43,2 L45,0 Z M64,54 L59,56 L62,52 L64,54 Z M75,50 L83,48 L78,50 L77,53 L73,50 L69,53 L67,48 L70,46 L75,50 Z M18,15 L18,13 L22,14 L18,15 Z M51,46 L48,46 L51,43 L51,46 Z'/> </g> <g class='hiroshima chugoku prefecture' data-code='34' stroke-linejoin='round' fill='" & If(nowSelect = 34, "#FFFF00", If(LookUp(Prefectures, Num = 34, PrefAns) <> "", "#FF0000", "#EEEEEE")) & "' fill-rule='nonzero' stroke='#000000' stroke-width='1.0' transform='translate(230.000000, 687.000000)'> <title>広島 / Hiroshima</title> <path d='M73,40 L72,42 L69,40 L72,43 L70,42 L68,47 L63,45 L63,42 L54,44 L51,48 L39,49 L37,53 L30,55 L28,53 L25,56 L26,53 L23,47 L25,45 L17,44 L9,51 L10,54 L5,52 L4,46 L2,44 L2,37 L0,35 L5,30 L7,24 L6,22 L12,17 L20,19 L22,16 L27,18 L35,16 L32,11 L38,8 L45,0 L58,2 L65,4 L68,7 L66,14 L70,19 L69,24 L73,40 Z M45,53 L43,56 L41,53 L46,51 L45,53 Z M21,54 L20,50 L23,51 L22,59 L19,59 L21,56 L17,51 L21,54 Z M15,48 L16,50 L12,52 L15,48 Z M35,57 L33,56 L36,56 L35,57 Z M24,58 L26,58 L25,61 L28,61 L26,63 L20,62 L23,56 L26,56 L24,58 Z M59,49 L59,52 L56,49 L59,49 Z M57,50 L53,52 L54,50 L57,50 Z M60,44 L62,45 L59,47 L60,44 Z'/> </g> <g class='okayama chugoku prefecture' data-code='33' stroke-linejoin='round' fill='" & If(nowSelect = 33, "#FFFF00", If(LookUp(Prefectures, Num = 33, PrefAns) <> "", "#FF0000", "#EEEEEE")) & "' fill-rule='nonzero' stroke='#000000' stroke-width='1.0' transform='translate(295.000000, 673.000000)'> <title>岡山 / Okayama</title> <polygon points='58 8 59 14 51 22 52 29 50 32 54 38 53 41 46 39 49 42 43 49 34 48 39 49 36 53 34 52 33 57 27 55 25 58 23 52 23 55 19 51 15 55 10 52 12 56 8 54 4 38 5 33 1 28 3 21 0 18 1 15 8 14 7 10 14 10 13 7 15 6 18 0 25 2 29 7 39 1 38 3 45 6 46 12'/> </g> <g class='shimane chugoku prefecture' data-code='32' stroke-linejoin='round' fill='" & If(nowSelect = 32, "#FFFF00", If(LookUp(Prefectures, Num = 32, PrefAns) <> "", "#FF0000", "#EEEEEE")) & "' fill-rule='nonzero' stroke='#000000' stroke-width='1.0' transform='translate(211.000000, 610.000000)'> <title>島根 / Shimane</title> <path d='M74,14 L78,13 L75,17 L72,15 L73,19 L71,16 L74,14 Z M79,16 L77,19 L77,15 L79,14 L81,17 L79,16 Z M83,5 L88,0 L93,5 L92,9 L89,9 L91,11 L86,12 L83,5 Z M2,100 L10,98 L25,84 L35,78 L41,70 L52,65 L55,59 L53,55 L70,51 L76,46 L79,49 L87,48 L84,50 L82,51 L88,59 L87,68 L79,70 L81,73 L77,79 L64,77 L57,85 L51,88 L54,93 L46,95 L41,93 L39,96 L31,94 L25,99 L26,101 L24,107 L19,112 L21,114 L17,116 L18,120 L15,124 L13,122 L7,124 L5,120 L7,115 L2,115 L0,111 L4,106 L2,100 Z'/> </g> <g class='tottori chugoku prefecture' data-code='31' stroke-linejoin='round' fill='" & If(nowSelect = 31, "#FFFF00", If(LookUp(Prefectures, Num = 31, PrefAns) <> "", "#FF0000", "#EEEEEE")) & "' fill-rule='nonzero' stroke='#000000' stroke-width='1.0' transform='translate(288.000000, 658.000000)'> <title>鳥取 / Tottori</title> <polygon points='7 33 0 31 4 25 2 22 10 20 11 11 5 3 7 2 9 6 15 8 23 4 34 6 55 5 64 0 71 17 71 21 65 23 53 27 52 21 45 18 46 16 36 22 32 17 25 15 22 21 20 22 21 25 14 25 15 29 8 30'/> </g> <g class='wakayama kinki prefecture' data-code='30' stroke-linejoin='round' fill='" & If(nowSelect = 30, "#FFFF00", If(LookUp(Prefectures, Num = 30, PrefAns) <> "", "#FF0000", "#EEEEEE")) & "' fill-rule='nonzero' stroke='#000000' stroke-width='1.0' transform='translate(385.000000, 737.000000)'> <title>和歌山 / Wakayama</title> <path d='M41,32 L44,38 L49,41 L45,46 L46,50 L38,55 L37,59 L35,59 L36,56 L19,51 L14,44 L17,41 L9,37 L4,30 L0,30 L3,25 L0,24 L6,21 L1,18 L5,16 L4,14 L8,14 L0,6 L2,3 L5,6 L31,0 L35,9 L31,10 L25,17 L30,24 L28,30 L41,32 Z M49,23 L49,26 L44,29 L44,26 L49,23 Z'/> </g> <g class='nara kinki prefecture' data-code='29' stroke-linejoin='round' fill='" & If(nowSelect = 29, "#FFFF00", If(LookUp(Prefectures, Num = 29, PrefAns) <> "", "#FF0000", "#EEEEEE")) & "' fill-rule='nonzero' stroke='#000000' stroke-width='1.0' transform='translate(410.000000, 712.000000)'> <title>奈良 / Nara</title> <polygon points='24 48 19 51 19 54 17 54 17 57 16 57 3 55 5 49 0 42 6 35 10 34 6 25 8 20 6 11 11 0 17 5 21 2 25 5 27 3 29 7 26 8 28 9 27 13 36 18 28 24 31 29 29 30 31 33 29 37 30 44 29 47'/> </g> <g class='hyogo kinki prefecture' data-code='28' stroke-linejoin='round' fill='" & If(nowSelect = 28, "#FFFF00", If(LookUp(Prefectures, Num = 28, PrefAns) <> "", "#FF0000", "#EEEEEE")) & "' fill-rule='nonzero' stroke='#000000' stroke-width='1.0' transform='translate(345.000000, 655.000000)'> <title>兵庫 / Hyogo</title> <path d='M31,90 L23,93 L23,88 L20,89 L20,86 L38,67 L39,69 L32,79 L35,88 L31,90 Z M3,59 L4,56 L0,50 L2,47 L1,40 L9,32 L8,26 L14,24 L14,20 L7,3 L16,0 L33,1 L32,5 L36,10 L41,9 L42,11 L42,17 L36,17 L35,23 L42,28 L47,26 L49,33 L59,35 L58,40 L55,41 L56,45 L62,48 L60,49 L62,60 L59,62 L53,60 L40,66 L25,56 L11,57 L11,53 L8,58 L5,56 L3,59 Z'/> </g> <g class='osaka kinki prefecture' data-code='27' stroke-linejoin='round' fill='" & If(nowSelect = 27, "#FFFF00", If(LookUp(Prefectures, Num = 27, PrefAns) <> "", "#FF0000", "#EEEEEE")) & "' fill-rule='nonzero' stroke='#000000' stroke-width='1.0' transform='translate(387.000000, 695.000000)'> <title>大阪 / Osaka</title> <polygon points='16 0 24 8 28 5 27 8 31 9 34 17 29 28 31 37 29 42 3 48 0 45 7 44 14 37 17 28 19 30 17 22 20 20 18 9 20 8 14 5 13 1'/> </g> <g class='kyoto kinki prefecture' data-code='26' stroke-linejoin='round' fill='" & If(nowSelect = 26, "#FFFF00", If(LookUp(Prefectures, Num = 26, PrefAns) <> "", "#FF0000", "#EEEEEE")) & "' fill-rule='nonzero' stroke='#000000' stroke-width='1.0' transform='translate(377.000000, 649.000000)'> <title>京都 / Kyoto</title> <polygon points='44 63 41 55 37 54 38 51 34 54 26 46 27 41 17 39 15 32 10 34 3 29 4 23 10 23 10 17 9 15 4 16 0 11 1 7 3 10 2 8 19 0 23 7 15 14 18 15 20 11 20 15 24 17 23 21 25 18 27 19 25 14 31 11 32 14 31 20 34 25 46 27 51 32 48 46 51 56 55 56 59 62 60 66 58 68 54 65 50 68'/> </g> <g class='shiga kinki prefecture' data-code='25' stroke-linejoin='round' fill='" & If(nowSelect = 25, "#FFFF00", If(LookUp(Prefectures, Num = 25, PrefAns) <> "", "#FF0000", "#EEEEEE")) & "' fill-rule='nonzero' stroke='#000000' stroke-width='1.0' transform='translate(423.000000, 655.000000)'> <title>滋賀 / Shiga</title> <path d='M20,0 L27,3 L28,9 L31,9 L34,16 L35,19 L31,29 L33,30 L34,40 L30,50 L24,53 L17,50 L16,55 L13,56 L9,50 L5,50 L2,40 L5,26 L0,21 L3,17 L6,18 L9,11 L12,13 L18,10 L18,7 L21,8 L20,0 Z M20,13 L20,16 L18,13 L14,16 L16,24 L8,32 L8,36 L5,42 L7,45 L9,36 L15,35 L16,31 L25,25 L26,21 L20,13 Z'/> </g> <g class='mie kinki prefecture' data-code='24' stroke-linejoin='round' fill='" & If(nowSelect = 24, "#FFFF00", If(LookUp(Prefectures, Num = 24, PrefAns) <> "", "#FF0000", "#EEEEEE")) & "' fill-rule='nonzero' stroke='#000000' stroke-width='1.0' transform='translate(426.000000, 683.000000)'> <title>三重 / Mie</title> <polygon points='8 95 3 92 0 86 1 86 1 83 3 83 8 80 8 77 13 76 14 73 13 66 15 62 13 59 15 58 12 53 20 47 11 42 12 38 10 37 13 36 11 32 10 28 13 27 14 22 21 25 27 22 31 12 30 2 36 0 43 7 48 15 42 16 43 20 36 32 35 40 51 47 53 52 56 51 55 55 50 55 55 56 55 61 51 62 53 60 50 58 49 60 43 61 46 57 42 57 39 62 39 60 36 63 34 61 34 64 32 62 25 66 22 68 23 73 21 72 22 70 18 73 22 77 20 77 22 80 19 78 20 81 13 85'/> </g> <g class='aichi chubu prefecture' data-code='23' stroke-linejoin='round' fill='" & If(nowSelect = 23, "#FFFF00", If(LookUp(Prefectures, Num = 23, PrefAns) <> "", "#FF0000", "#EEEEEE")) & "' fill-rule='nonzero' stroke='#000000' stroke-width='1.0' transform='translate(469.000000, 673.000000)'> <title>愛知 / Aichi</title> <polygon points='42 46 18 52 20 47 23 49 31 43 33 46 35 43 34 40 28 37 26 41 18 40 14 37 16 29 12 40 16 45 13 45 9 41 10 36 8 33 8 28 12 21 5 25 0 17 1 11 5 3 15 0 21 8 26 10 33 8 39 12 46 8 47 14 52 11 60 13 50 33 42 38'/> </g> <g class='shizuoka chubu prefecture' data-code='22' stroke-linejoin='round' fill='" & If(nowSelect = 22, "#FFFF00", If(LookUp(Prefectures, Num = 22, PrefAns) <> "", "#FF0000", "#EEEEEE")) & "' fill-rule='nonzero' stroke='#000000' stroke-width='1.0' transform='translate(511.000000, 659.000000)'> <title>静岡 / Shizuoka</title> <polygon points='73 14 77 14 77 27 83 31 81 36 85 42 85 46 78 55 77 61 70 65 64 59 67 45 65 41 66 38 72 37 62 31 55 33 51 38 53 41 44 45 42 54 36 61 38 65 25 61 0 60 0 52 8 47 18 27 33 17 33 5 37 0 40 8 39 19 44 20 48 29 53 27 52 20 56 12 61 17'/> </g> <g class='gifu chubu prefecture' data-code='21' stroke-linejoin='round' fill='" & If(nowSelect = 21, "#FFFF00", If(LookUp(Prefectures, Num = 21, PrefAns) <> "", "#FF0000", "#EEEEEE")) & "' fill-rule='nonzero' stroke='#000000' stroke-width='1.0' transform='translate(450.000000, 608.000000)'> <title>岐阜 / Gifu</title> <polygon points='66 4 69 10 64 20 67 24 66 28 60 35 55 35 53 40 59 43 64 50 62 53 68 59 66 63 68 66 65 66 67 70 65 73 58 77 52 73 45 75 40 73 34 65 24 68 20 76 19 82 12 75 6 77 4 76 8 66 7 63 4 56 1 56 0 50 5 41 11 44 12 42 25 41 28 37 22 29 24 23 29 13 26 10 30 5 34 7 35 11 44 0 47 2 52 0 52 2 56 0'/> </g> <g class='nagano chubu prefecture' data-code='20' stroke-linejoin='round' fill='" & If(nowSelect = 20, "#FFFF00", If(LookUp(Prefectures, Num = 20, PrefAns) <> "", "#FF0000", "#EEEEEE")) & "' fill-rule='nonzero' stroke='#000000' stroke-width='1.0' transform='translate(503.000000, 572.000000)'> <title>長野 / Nagano</title> <polygon points='68 18 59 21 60 23 55 27 54 34 57 39 66 39 67 45 64 47 66 53 63 54 66 57 66 63 70 65 71 70 68 72 64 69 58 71 53 66 43 77 46 80 43 82 45 87 41 92 41 104 26 114 18 112 13 115 12 109 14 106 12 102 15 102 13 99 15 95 9 89 11 86 6 79 0 76 2 71 7 71 13 64 14 60 11 56 16 46 13 40 19 32 18 29 21 28 22 17 27 10 27 7 34 8 34 13 36 14 44 10 48 12 48 7 53 2 60 0 63 2 63 7 68 11'/> </g> <g class='yamanashi chubu prefecture' data-code='19' stroke-linejoin='round' fill='" & If(nowSelect = 19, "#FFFF00", If(LookUp(Prefectures, Num = 19, PrefAns) <> "", "#FF0000", "#EEEEEE")) & "' fill-rule='nonzero' stroke='#000000' stroke-width='1.0' transform='translate(546.000000, 638.000000)'> <title>山梨 / Yamanashi</title> <polygon points='2 21 0 16 3 14 0 11 10 0 15 5 21 3 25 6 28 4 39 7 43 15 48 18 47 27 38 35 26 38 21 33 17 41 18 48 13 50 9 41 4 40 5 29'/> </g> <g class='fukui chubu prefecture' data-code='18' stroke-linejoin='round' fill='" & If(nowSelect = 18, "#FFFF00", If(LookUp(Prefectures, Num = 18, PrefAns) <> "", "#FF0000", "#EEEEEE")) & "' fill-rule='nonzero' stroke='#000000' stroke-width='1.0' transform='translate(408.000000, 618.000000)'> <title>福井 / Fukui</title> <polygon points='40 0 46 8 56 9 61 14 66 13 64 19 70 27 67 31 54 32 53 34 47 31 42 40 35 37 36 45 33 44 33 47 27 50 24 48 21 55 18 54 15 58 3 56 0 51 1 45 3 49 10 46 6 49 13 50 15 47 13 48 13 45 19 47 17 45 20 43 18 40 26 41 25 35 28 33 29 38 31 39 32 32 25 19 34 6 34 2'/> </g> <g class='ishikawa chubu prefecture' data-code='17' stroke-linejoin='round' fill='" & If(nowSelect = 17, "#FFFF00", If(LookUp(Prefectures, Num = 17, PrefAns) <> "", "#FF0000", "#EEEEEE")) & "' fill-rule='nonzero' stroke='#000000' stroke-width='1.0' transform='translate(448.000000, 541.000000)'> <title>石川 / Ishikawa</title> <path d='M37,26 L34,23 L41,23 L37,26 Z M28,77 L31,80 L26,90 L21,91 L16,86 L6,85 L0,77 L15,62 L26,44 L27,33 L25,29 L25,24 L22,23 L27,10 L52,0 L56,1 L57,5 L51,6 L50,15 L46,14 L39,22 L37,18 L34,19 L32,28 L38,30 L41,26 L41,36 L33,38 L31,48 L28,50 L30,54 L28,57 L29,61 L27,69 L28,77 Z'/> </g> <g class='toyama chubu prefecture' data-code='16' stroke-linejoin='round' fill='" & If(nowSelect = 16, "#FFFF00", If(LookUp(Prefectures, Num = 16, PrefAns) <> "", "#FF0000", "#EEEEEE")) & "' fill-rule='nonzero' stroke='#000000' stroke-width='1.0' transform='translate(475.000000, 575.000000)'> <title>富山 / Toyama</title> <polygon points='41 37 31 33 27 35 27 33 22 35 19 33 10 44 9 40 5 38 1 43 0 35 2 27 1 23 3 20 1 16 4 14 6 4 14 2 11 8 23 15 31 11 33 3 43 0 47 3 50 14 49 25 46 26 47 29'/> </g> <g class='niigata chubu prefecture' data-code='15' stroke-linejoin='round' fill='" & If(nowSelect = 15, "#FFFF00", If(LookUp(Prefectures, Num = 15, PrefAns) <> "", "#FF0000", "#EEEEEE")) & "' fill-rule='nonzero' stroke='#000000' stroke-width='1.0' transform='translate(518.000000, 476.000000)'> <title>新潟 / Niigata</title> <path d='M7,113 L4,102 L0,99 L14,94 L23,87 L30,87 L45,75 L55,60 L60,46 L79,34 L80,36 L79,35 L85,28 L94,0 L102,3 L102,10 L111,16 L109,20 L101,23 L101,31 L98,39 L101,44 L104,46 L95,56 L97,65 L89,65 L88,68 L79,69 L78,72 L80,76 L76,82 L81,87 L80,101 L73,94 L71,98 L67,99 L68,104 L65,105 L65,108 L53,114 L53,107 L48,103 L48,98 L45,96 L38,98 L33,103 L33,108 L29,106 L21,110 L19,109 L19,104 L12,103 L12,106 L7,113 Z M36,46 L28,47 L35,38 L33,35 L30,37 L30,31 L34,23 L43,14 L39,30 L46,31 L43,40 L36,46 Z'/> </g> <g class='kanagawa kanto prefecture' data-code='14' stroke-linejoin='round' fill='" & If(nowSelect = 14, "#FFFF00", If(LookUp(Prefectures, Num = 14, PrefAns) <> "", "#FF0000", "#EEEEEE")) & "' fill-rule='nonzero' stroke='#000000' stroke-width='1.0' transform='translate(584.000000, 656.000000)'> <title>神奈川 / Kanagawa</title> <polygon points='10 0 24 6 28 11 28 6 30 6 27 4 30 2 44 9 40 13 36 12 39 16 36 16 36 23 42 26 38 30 39 33 35 33 36 28 33 24 26 22 15 25 11 28 12 34 10 34 4 30 4 17 0 17 9 9'/> </g> <g class='tokyo kanto prefecture' data-code='13' stroke-linejoin='round' fill='" & If(nowSelect = 13, "#FFFF00", If(LookUp(Prefectures, Num = 13, PrefAns) <> "", "#FF0000", "#EEEEEE")) & "' fill-rule='nonzero' stroke='#000000' stroke-width='1.0' transform='translate(585.000000, 642.000000)'> <title>東京 / Tokyo</title> <path d='M49,173 L49,178 L44,171 L49,173 Z M34,113 L32,115 L29,114 L31,111 L34,113 Z M11,104 L13,106 L11,107 L11,104 Z M18,98 L16,96 L18,92 L18,98 Z M22,75 L22,69 L26,70 L26,76 L22,75 Z M48,7 L49,12 L47,16 L41,15 L43,19 L39,20 L43,20 L43,23 L29,16 L26,18 L29,20 L27,20 L27,25 L23,20 L9,14 L4,11 L0,3 L3,0 L18,4 L23,8 L30,5 L30,9 L36,6 L40,7 L42,5 L48,7 Z'/> </g> <g class='chiba kanto prefecture' data-code='12' stroke-linejoin='round' fill='" & If(nowSelect = 12, "#FFFF00", If(LookUp(Prefectures, Num = 12, PrefAns) <> "", "#FF0000", "#EEEEEE")) & "' fill-rule='nonzero' stroke='#000000' stroke-width='1.0' transform='translate(627.000000, 629.000000)'> <title>千葉 / Chiba</title> <polygon points='5 29 7 25 6 20 6 13 0 0 8 10 19 15 37 11 37 8 55 20 55 24 45 24 34 35 32 57 19 61 7 75 0 71 5 69 3 66 4 63 3 58 5 53 1 49 4 45 7 46 7 42 13 39 18 33 11 26 6 30'/> </g> <g class='saitama kanto prefecture' data-code='11' stroke-linejoin='round' fill='" & If(nowSelect = 11, "#FFFF00", If(LookUp(Prefectures, Num = 11, PrefAns) <> "", "#FF0000", "#EEEEEE")) & "' fill-rule='nonzero' stroke='#000000' stroke-width='1.0' transform='translate(573.000000, 618.000000)'> <title>埼玉 / Saitama</title> <polygon points='48 4 49 5 51 12 54 11 54 11 60 24 60 31 54 29 52 31 48 30 42 33 42 29 35 32 30 28 15 24 12 27 1 24 0 19 16 10 21 0 32 2 38 6'/> </g> <g class='gunma kanto prefecture' data-code='10' stroke-linejoin='round' fill='" & If(nowSelect = 10, "#FFFF00", If(LookUp(Prefectures, Num = 10, PrefAns) <> "", "#FF0000", "#EEEEEE")) & "' fill-rule='nonzero' stroke='#000000' stroke-width='1.0' transform='translate(557.000000, 570.000000)'> <title>群馬 / Gunma</title> <polygon points='64 52 54 54 48 50 37 48 32 58 16 67 12 65 12 59 9 56 12 55 10 49 13 47 12 41 3 41 0 36 1 29 6 25 5 23 14 20 26 14 26 11 29 10 28 5 32 4 34 0 41 7 49 9 47 12 50 14 47 17 46 26 54 29 48 42 53 49 62 48'/> </g> <g class='tochigi kanto prefecture' data-code='9' stroke-linejoin='round' fill='" & If(nowSelect = 9, "#FFFF00", If(LookUp(Prefectures, Num = 9, PrefAns) <> "", "#FF0000", "#EEEEEE")) & "' fill-rule='nonzero' stroke='#000000' stroke-width='1.0' transform='translate(603.000000, 563.000000)'> <title>栃木 / Tochigi</title> <polygon points='19 60 18 59 16 55 7 56 2 49 8 36 0 33 1 24 4 21 1 19 3 16 28 0 40 3 46 7 47 13 47 24 48 27 45 29 47 39 44 46 33 48 30 53 27 53 26 57'/> </g> <g class='ibaraki kanto prefecture' data-code='8' stroke-linejoin='round' fill='" & If(nowSelect = 8, "#FFFF00", If(LookUp(Prefectures, Num = 8, PrefAns) <> "", "#FF0000", "#EEEEEE")) & "' fill-rule='nonzero' stroke='#000000' stroke-width='1.0' transform='translate(622.000000, 575.000000)'> <title>茨城 / Ibaraki</title> <polygon points='5 54 5 54 2 55 0 48 7 45 8 41 11 41 14 36 25 34 28 27 26 17 29 15 28 12 28 1 38 10 44 4 44 0 54 5 46 29 47 35 44 42 46 52 51 61 49 63 51 66 52 62 60 74 42 62 42 65 24 69 13 64'/> </g> <g class='fukushima tohoku prefecture' data-code='7' stroke-linejoin='round' fill='" & If(nowSelect = 7, "#FFFF00", If(LookUp(Prefectures, Num = 7, PrefAns) <> "", "#FF0000", "#EEEEEE")) & "' fill-rule='nonzero' stroke='#000000' stroke-width='1.0' transform='translate(594.000000, 511.000000)'> <title>福島 / Fukushima</title> <polygon points='82 69 72 64 72 68 66 74 56 65 55 59 49 55 37 52 12 68 4 66 5 52 0 47 4 41 2 37 3 34 12 33 13 30 21 30 19 21 28 11 25 9 32 11 38 10 40 14 44 13 47 15 53 14 56 10 54 9 55 0 61 0 65 4 74 4 75 10 80 12 79 10 83 10 83 4 87 4 93 18 93 45 91 61'/> </g> <g class='yamagata tohoku prefecture' data-code='6' stroke-linejoin='round' fill='" & If(nowSelect = 6, "#FFFF00", If(LookUp(Prefectures, Num = 6, PrefAns) <> "", "#FF0000", "#EEEEEE")) & "' fill-rule='nonzero' stroke='#000000' stroke-width='1.0' transform='translate(612.000000, 439.000000)'> <title>山形 / Yamagata</title> <polygon points='14 1 24 0 28 5 42 8 44 13 48 14 53 22 51 30 48 30 52 41 45 53 46 59 43 65 36 67 37 72 36 81 38 82 35 86 29 87 26 85 22 86 20 82 14 83 7 81 4 76 7 68 7 60 15 57 17 53 8 47 8 40 0 37 13 14'/> </g> <g class='akita tohoku prefecture' data-code='5' stroke-linejoin='round' fill='" & If(nowSelect = 5, "#FFFF00", If(LookUp(Prefectures, Num = 5, PrefAns) <> "", "#FF0000", "#EEEEEE")) & "' fill-rule='nonzero' stroke='#000000' stroke-width='1.0' transform='translate(617.000000, 352.000000)'> <title>秋田 / Akita</title> <polygon points='54 97 47 102 43 101 39 100 37 95 23 92 19 87 9 88 11 78 16 69 17 47 11 39 3 42 0 33 6 36 12 28 15 19 15 11 10 6 14 7 17 3 29 5 32 2 39 7 42 5 45 7 55 0 55 5 60 5 59 16 55 21 57 39 52 40 55 44 52 48 54 53 50 59 47 70 55 83 52 86 55 91'/> </g> <g class='miyagi tohoku prefecture' data-code='4' stroke-linejoin='round' fill='" & If(nowSelect = 4, "#FFFF00", If(LookUp(Prefectures, Num = 4, PrefAns) <> "", "#FF0000", "#EEEEEE")) & "' fill-rule='nonzero' stroke='#000000' stroke-width='1.0' transform='translate(648.000000, 445.000000)'> <title>宮城 / Miyagi</title> <path d='M33,70 L29,70 L29,76 L25,76 L26,78 L21,76 L20,70 L11,70 L7,66 L1,66 L0,61 L7,59 L10,53 L9,47 L16,35 L12,24 L15,24 L17,16 L12,8 L16,9 L23,4 L33,9 L41,8 L39,12 L45,16 L49,11 L55,14 L57,0 L64,1 L67,8 L62,5 L63,10 L59,14 L62,19 L60,17 L56,20 L60,22 L57,28 L61,28 L61,32 L58,30 L60,34 L57,35 L58,38 L61,37 L59,39 L61,41 L61,45 L57,43 L58,40 L55,41 L57,39 L56,37 L39,40 L36,43 L40,45 L35,46 L38,47 L32,57 L33,70 Z M43,41 L44,43 L42,43 L43,41 Z'/> </g> <g class='iwate tohoku prefecture' data-code='3' stroke-linejoin='round' fill='" & If(nowSelect = 3, "#FFFF00", If(LookUp(Prefectures, Num = 3, PrefAns) <> "", "#FF0000", "#EEEEEE")) & "' fill-rule='nonzero' stroke='#000000' stroke-width='1.0' transform='translate(664.000000, 354.000000)'> <title>岩手 / Iwate</title> <polygon points='48 92 41 91 39 105 33 102 29 107 23 103 25 99 17 100 7 95 8 89 5 84 8 81 0 68 3 57 7 51 5 46 8 42 5 38 10 37 8 19 12 14 16 15 29 5 31 8 35 5 40 7 46 0 54 11 52 16 57 19 55 23 61 28 64 43 62 53 65 49 66 54 68 55 62 60 64 62 67 59 67 63 63 63 63 68 60 69 65 68 60 71 62 73 60 75 64 74 58 81 62 83 57 83 60 86 57 86 59 88 53 89 52 86 54 91 52 91 52 94 50 90'/> </g> <g class='aomori tohoku prefecture' data-code='2' stroke-linejoin='round' fill='" & If(nowSelect = 2, "#FFFF00", If(LookUp(Prefectures, Num = 2, PrefAns) <> "", "#FF0000", "#EEEEEE")) & "' fill-rule='nonzero' stroke='#000000' stroke-width='1.0' transform='translate(624.000000, 287.000000)'> <title>青森 / Aomori</title> <polygon points='3 71 3 63 0 60 6 51 12 51 18 47 21 31 17 27 20 26 21 19 27 23 31 20 35 23 37 38 40 45 46 41 45 37 47 33 58 42 61 39 65 23 61 16 55 22 41 25 47 0 64 11 73 6 71 37 73 51 77 62 81 61 86 67 80 74 75 72 71 75 69 72 56 82 52 81 53 70 48 70 48 65 38 72 35 70 32 72 25 67 22 70 10 68 7 72'/> </g> <g class='hokkaido prefecture' data-code='1' stroke-linejoin='round' fill='" & If(nowSelect = 1, "#FFFF00", If(LookUp(Prefectures, Num = 1, PrefAns) <> "", "#FF0000", "#EEEEEE")) & "' fill-rule='nonzero' stroke='#000000' stroke-width='1.0' transform='translate(602.000000, 10.000000)'> <title>北海道 / Hokkaido</title> <path d='M4,240 L3,245 L0,246 L0,237 L6,235 L4,240 Z M33,261 L32,250 L28,243 L23,242 L21,237 L17,236 L15,231 L19,223 L17,212 L19,209 L28,207 L34,198 L37,202 L39,201 L43,192 L49,187 L39,173 L40,166 L47,164 L60,174 L71,171 L71,174 L78,177 L83,174 L89,165 L83,140 L86,135 L93,132 L97,126 L96,103 L100,95 L101,85 L98,67 L90,48 L93,39 L92,33 L94,36 L99,35 L105,28 L131,55 L139,68 L155,85 L184,104 L213,109 L214,113 L219,118 L238,118 L260,91 L262,96 L252,119 L252,129 L255,135 L265,138 L263,140 L264,137 L258,137 L263,149 L269,156 L273,157 L280,149 L287,148 L277,156 L275,163 L258,166 L256,172 L252,177 L247,177 L245,175 L246,173 L243,172 L240,178 L243,180 L228,181 L220,178 L205,186 L191,202 L182,216 L179,225 L180,240 L178,248 L164,237 L141,228 L113,211 L100,209 L103,206 L88,214 L72,230 L69,227 L73,227 L68,226 L66,220 L58,212 L47,213 L42,220 L39,230 L40,234 L52,242 L62,242 L71,254 L80,257 L82,260 L76,265 L72,267 L63,263 L60,265 L60,261 L57,260 L55,265 L48,269 L48,278 L40,282 L37,287 L30,284 L27,278 L28,269 L33,261 Z M71,48 L73,45 L77,47 L79,52 L75,55 L71,52 L71,48 Z M65,35 L65,33 L67,35 L66,45 L63,35 L65,35 Z M369,17 L367,13 L365,13 L363,17 L365,23 L364,26 L359,28 L357,35 L350,34 L351,41 L341,50 L341,54 L335,54 L335,56 L339,58 L339,62 L336,65 L332,64 L332,69 L329,66 L330,71 L327,78 L331,79 L336,70 L341,69 L346,59 L356,50 L358,40 L363,43 L369,41 L384,24 L397,15 L406,13 L407,10 L404,6 L407,2 L402,0 L396,2 L384,20 L373,22 L369,17 Z M290,99 L295,93 L303,91 L308,84 L311,85 L314,79 L304,82 L296,77 L293,79 L289,89 L280,104 L280,107 L266,122 L268,129 L273,128 L273,132 L274,119 L282,114 L283,109 L286,109 L287,102 L290,99 Z M322,125 L334,115 L329,113 L325,116 L326,117 L319,119 L319,123 L321,122 L322,125 Z M300,142 L304,137 L297,139 L300,142 Z M291,146 L293,143 L289,144 L291,146 Z'/> </g> </g> <g class='boundary-line' stroke='#EEEEEE' stroke-width='12' stroke-linejoin='round' style=''> <line x1='320.227' y1='361.996' x2='582.351' y2='109.378' style=''/> <line x1='277.337' y1='380.162' x2='46.213' y2='380.162' style=''/> </g> </g> </svg>" )

追加した画像コントロールは画面いっぱいに表示しておいてください。
これで日本地図が画面右側に表示することができました。

fill='" & If(nowSelect = 47, "#FFFF00", If(LookUp(Prefectures, Num = 47, PrefAns) <> "", "#FF0000", "#EEEEEE")) & "'

としているのは、対象の県の色をユーザーの操作の状態で変化させるためです。

対象の県が選択されているときは黄色      
対象の県の答えが既に入力されているときは赤色 
上記以外の時は灰色              

をそれぞれ表示されるように設定しています。

OnVisibele で設定した、変数 / コレクションですが、それぞれ以下の用途で使用しています。

変数名 種類 使用用途
scoring 変数 スコア表示を行うか
nowSelect 変数 現在選択されている県の番号
Prefectures コレクション 都道府県の番号、名称、ユーザーが入力した回答

回答入力欄を作成する

ユーザーが回答を入力するようの回答入力欄を作成します。

こちらの回答入力欄ですが、画像のように
01 11
︙ ︙
10 20
のようにN 順で回答欄を今回は作成させたいと思います。

しかし、ギャラリー(縦方向)の Items に先ほどの Prefecture を設定し、 WrapCount にて折り返し表示を行いたい数だけ任意の数値を設定した場合、
01…10
11…20
のようにZ 順で表示されてしまいます。

ギャラリー(横方向)を利用し、 WrapCount を設定した場合は上記のようにN 順で回答欄を作成することが可能です。
ただし WrapCount にて設定できる数は最大で 10 です。
今回47都道府県を3行で表示させたかったので、一列あたりに表示させる数は最大で16ですので、条件を満たせません。

また、県の他に県庁所在地の入力欄もユーザーの選択したモードで 表示 / 非表示 を制御したいので、「伸縮可能」なギャラリーを用いる必要があります。
この伸縮可能ギャラリーは縦方向のものしか現時点(2021/1/1)では存在していません。
よって縦方向のギャラリーでどうにか実現する必要があります。

なお伸縮可能ギャラリーに関しては、Hiro さんのこちらの記事を参考にしてください。
Flexible Height Gallery (高さ可変ギャラリー)のススメ

そこで今回は各列ごとにギャラリーを設定することにしました。
今回は3行表示させたいので、ギャラリーを3つ設定することになりますね。

伸縮可能ギャラリーを1つ追加して Items に以下を設定します。

Gallery.Items

Filter(Prefectures, Num <= 16)

他2つのギャラリーと設定が異なるのは、この Items の項目と座標の設定だけですので、先に1つ目の列を作成して残りはコピペで作成しましょう。

ギャラリーには以下のコントロールを設定します。

f:id:koruneko:20210101031911p:plain

それぞれ以下のような役割をもっています。

コントロール 役割
NumLabel 番号を表示
PrefTextInput 都道県回答入力欄
PrefCapTextInput 県庁所在地回答入力欄
PrefAns 都道府県回答
PrefCapAns 県庁所在地回答

追加したらそれぞれ以下のように設定しましょう!
(座標の設定は省いていますので、各自適宜揃えてください。)

NumLabel

NumLabel.Label

ThisItem.Num

PrefTextInput

PrefTextInput.Default

ThisItem.PrefAns

PrefTextInput.HintText

"都道府県名"

PrefTextInput.DisplayMode

If(!scoring, DisplayMode.Edit, View)

PrefTextInput.OnChange

UpdateIf(Prefectures, Num = ThisItem.Num, {PrefAns:Self.Text});
If(!Toggle1.Value, UpdateContext({nowSelect:ThisItem.Num + 1}))

PrefTextInput.OnSelect

Select(Parent);
UpdateContext({nowSelect:ThisItem.Num})

PrefCapTextInput

PrefCapTextInput.Default

ThisItem.PrefCapAns

PrefCapTextInput.HintText

"県庁所在地"

PrefCapTextInput.DisplayMode

If(!scoring, DisplayMode.Edit, View)

PrefCapTextInput.OnChange

UpdateIf(Prefectures, Num = ThisItem.Num, {PrefCapAns:Self.Text});
UpdateContext({nowSelect:ThisItem.Num + 1})

PrefCapTextInput.OnSelect

Select(Parent);
UpdateContext({nowSelect:ThisItem.Num})

PrefCapTextInput.Visible

Toggle1.Value

PrefAns
PrefAns.Text

ThisItem.Pref

PrefAns.Visible

scoring

PrefAns.Color

RGBA(0, 0, 0, 0)

PrefAns.Fill

RGBA(0, 0, 0, 0)

PrefAns.HoverColor

If(ThisItem.Pref = ThisItem.PrefAns, Self.Color, RGBA(0, 0, 0, 1))

PrefAns.HoverFill

If(ThisItem.Pref = ThisItem.PrefAns, Self.Fill, RGBA(220, 20, 60, 1))

PrefAns.OnSelect

UpdateContext({nowSelect:ThisItem.Num})

PrefCapAns
PrefCapAns.Text

ThisItem.PrefCap

PrefCapAns.Visible

scoring

PrefCapAns.Color

RGBA(0, 0, 0, 0)

PrefCapAns.Fill

RGBA(0, 0, 0, 0)

PrefCapAns.HoverColor

If(ThisItem.PrefCap = ThisItem.PrefCapAns, Self.Color, RGBA(0, 0, 0, 1))

PrefCapAns.HoverFill

If(ThisItem.PrefCap = ThisItem.PrefCapAns, Self.Fill, RGBA(220, 20, 60, 1))

PrefCapAns.OnSelect

UpdateContext({nowSelect:ThisItem.Num})

Toggle は県庁所在地を問題に表示するか否かの選択肢です。
私の場合は初回のHome画面で設定しています。

テキスト入力欄の OnChangeUpdateContext({nowSelect:ThisItem.Num + 1}) としているのは、タブ遷移を行って次の入力欄に移った時 OnSelect が働かないからです。
タブ遷移を行ったときは次の入力欄にいきたいということなので、疑似的に実現するために上記のように設定しています。

Power Apps でのタブ遷移の動きについては備忘録も兼ねて今度纏めたいと思います。

回答を表示しているラベルはテキスト入力の座標、大きさとそれぞれ揃えて設定してください。
回答はマウスホバーしたときに表示されるようにしたいので、デフォルト時に表示される色は透明とし、ホバー時に色が設定されるようにしています。

これで回答入力欄は完成なので、残り2列分もコピーして、座標を合わせましょう。
Items にはそれぞれ以下のように設定します。

Gallery_2.Items

Filter(Prefectures, Num > 16, Num <= 32)

Gallery_3.Items

Filter(Prefectures, Num > 32)

採点ボタンを作成する

最後に採点ボタンと採点結果を表示するラベルを追加しましょう。

採点ボタンでは以下のように設定します。

Button.OnSelect

UpdateContext({scoring:true});

採点結果ラベルには以下のように設定します。

Label.Text

"都道府県 :" & If(!scoring, "    ", Text(CountIf(Prefectures, Pref = PrefAns), "[$-ja]00")) & "/ 47" & Char(10) &
If(Toggle1.Value, "県庁所在地:" & If(!scoring, "    ", Text(CountIf(Prefectures, PrefCap = PrefCapAns), "[$-ja]00")) & "/ 47" & Char(10)) &
If(scoring, "カーソルを合わせると回答を表示します。")

Char(10) で改行を表します。

以上で日本地図のクイズの作成は完了です!

おわりに

これを利用すれば他チリのクイズも作成が可能です。
またスコアをShare Point リストに設定して、記録するようにするのも面白そうですね。

もし作成してみた方がいらっしゃいましたら教えていただけますと嬉しいです!

ローカルのExcel ファイルをSharePoint リストにアップロードしてPower Apps で表示する

はじめに

以下のようなコメントをいただきましたのでやり方を簡単に紹介したいと思います。

PowerAppsでローカルのエクセルをSharepointリストやDBにアップロードしてそれをPowerAppsで表示したいです。やり方などもし分かればご紹介してほしいです。(PowerAutomateを呼び出して使う?)

SharePoint リストにアップする際のExcel ファイルの前提条件

ローカルのExcel ファイルをSharePoint リストにアップロードする際、Excel ファイルはある前提条件を満たしている必要があります。
それは、対象データが"テーブル"内に存在している必要があるということです。

対象ファイルにテーブルが存在しない場合、以下のようなメッセージが表示されます。

f:id:koruneko:20201231230405p:plain

Excel ファイルにテーブルを設定する

では、Excel ファイルにテーブルを設定する方法を紹介します。
知っている方は飛ばしてもらって大丈夫です。

例えば以下のような表がExcel に存在していたとします。

f:id:koruneko:20201231230601p:plain

この表をテーブルに設定してみます。

  1. まずは対象の列(今回であれば $A$1:$C$14 )して「挿入 > テーブル」を選択、もしくは"Ctrl + T"を押します。
    f:id:koruneko:20201231234439p:plain
  2. すると以下のようなメッセージがでてくるので"OK"を選択します。
    先頭行もデータとして利用するのであれば、「先頭行をテーブルの見出しとして使用する(M)」のチェックを外してください。
    f:id:koruneko:20201231234518p:plain
  3. これでテーブルの設定が完了しました。
    デザインやテーブル名を変更したいときは、「テーブルデザイン」より変更してください。
    f:id:koruneko:20201231232921p:plain

以上でテーブルの設定は完了です。

なお、注意として A:C のように選択してテーブルに設定するのはやめましょう。
読み込みの際に、読み込み範囲のエラーが表示されてしまいます。

Power Automate などで読み込む場合もテーブルの設定が必要なのでこのやり方は是非覚えておきましょう。

ローカルのExcel ファイルをSharePoint リストにアップロードする

  1. リストを作成したいSharePoint サイトを開いて「新規 > リスト」を選択します。
    f:id:koruneko:20201231233256p:plain
  2. Excel から」を選択します。
    f:id:koruneko:20201231233531p:plain
  3. 今回ローカルからアップロードしたいので「ファイルをアップロードする」を選択します。
    f:id:koruneko:20201231233717p:plain
  4. 問題なければ「次へ」を選択します。
    f:id:koruneko:20201231234753p:plain
  5. 名前などを設定したら、「作成」を選択します。
    f:id:koruneko:20201231234859p:plain

以上でローカルのExcel ファイルをSharePoint リストにアップロードすることができました!

f:id:koruneko:20201231234949p:plain

Power Apps でSharePoint リストを表示する

Power Apps でSharePoint リストを表示する方法は何通りかありますが、一番簡単な方法を紹介します。

先ほど作成したリストより、「PowerApps > アプリの作成」を選択します。

f:id:koruneko:20201231235624p:plain

適当な名前をつけます。

f:id:koruneko:20201231235744p:plain

たったこれだけで、アプリの作成ができます!!

f:id:koruneko:20201231235900p:plain

おわりに

以上で紹介は終了です。
他質問あればコメントにてお願いしますー。

ではよいお年を。

【Power Apps】糸通しゲームを作成してみる

はじめに

みなさん"糸通し"というアプリをご存じでしょうか?

play.google.com

元祖糸通し

元祖糸通し

  • Spicysoft Corp.
  • ゲーム
  • 無料
apps.apple.com

今回は上記のようなアプリをPower Apps で作成してみましたので作成方法を纏めてみようと思います!

糸通しゲームを作成する

糸を描画する

糸のアニメーションはSVG で描画させたいと思います。

糸の情報は trajectory というコレクションに格納したいと思います。

trajectory には1ms単位のx座標、y座標、ボタンが押されたか?の情報を保持させようと思います。

n - 1 レコード目とn レコード目、n + 1レコード目の点をそれぞれ繋ぐように線を引くことで糸を作成しています。

以下のようにコレクションの初期化、糸の描画のための記載をしましょう。

PlayScreen.OnVisible

ClearCollect(trajectory, {x:0, y:PlayScreen.Height / 2, press:false});

Image.Image

"data:image/svg+xml,"& 
EncodeUrl(
    "<svg viewBox='0 0 "& Self.Width & " " & Self.Height & "' xmlns='http://www.w3.org/2000/svg'>
        <path d='" &
        Concat(
            ForAll(
                Sequence(CountRows(trajectory)),
                If(
                    Value = 1,
                    "M " & First(trajectory).x & " " & First(trajectory).y,
                    If(Last(FirstN(trajectory, Value)).press = Last(FirstN(trajectory, Value + 1)).press,
                        " L" & Last(FirstN(trajectory, Value)).x & " " & Last(FirstN(trajectory, Value)).y,
                        " S" & Last(FirstN(trajectory, Value)).x & " " & 
                               Last(FirstN(trajectory, Value)).y & " " &
                               Last(FirstN(trajectory, Value + 1)).x & " " & 
                               Last(FirstN(trajectory, Value + 1)).y
                    )
                )
            ),
            Value  
        )
        & "
    </svg>"
)

初期位置はx座標は0、y座標は画面の中心にくるように設定しています。

心の糸の描画箇所ですが、先ほども記載したように点と点を繋ぐようにして描画させています。

ただ点と点を繋いだだけでは線のUp/Down の切り替えタイミングが滑らかな線にならず切替点が尖ったものになってしまいますので、切替点は丸く、滑らかになるようにしたいと思います。
それを実現しているのが、

" S" & Last(FirstN(trajectory, Value)).x & " " & 
       Last(FirstN(trajectory, Value)).y & " " &
       Last(FirstN(trajectory, Value + 1)).x & " " & 
       Last(FirstN(trajectory, Value + 1)).y

です。

Up/Down が切り替わるのは、Nレコード目とN + 1レコード目のPress が異なっていた場合です。
それを、 Last(FirstN(trajectory, Value)).press = Last(FirstN(trajectory, Value + 1)).press を条件にして上記を実現させています。

trajectory にレコードを追加する

trajectory の情報によって糸は作成されます。

このコレクションのレコードは1ms単位で更新していきたいと思います。

なので、Duration:1のタイマーを追加し、以下のように設定します。

Timer.OnTimerStart

Collect(
    trajectory, 
    {
        x:If(Last(trajectory).x >= PlayScreen.Width / 2, Last(trajectory).x, Last(trajectory).x + PlayScreen.Width / 100), 
        y:Last(trajectory).y + If(Button1.Pressed, -1, 1) * PlayScreen.Height / 300,
        press:Button1.Pressed
    }
);

また、Up/Down を切り替えるためのボタンも作成しておいてください。

糸が動いているようにみせるために、xは時間経過で最後のレコードのx座標にプラスした値を設定していきます。
ただし、ずっとx座標の値をプラスし続けていると、画面からはみでてしまいます。
なので画面中央に来たタイミングでx座標の値のプラスは行わないようにしています。

y座標は最後の値にボタンが押されていないときはプラスし、ボタンが押されているときはマイナスします。

このままでは画面中央まで糸が到達した後、線が想定通り引かれません。

よってタイマー終了時、糸が画面中央まできていた場合以下のようにしてx座標の値を更新したいと思います

Timer.OnTimerEnd

If(
    CountRows(trajectory) >= 50, 
    RemoveIf(trajectory, x = First(trajectory).x, y = First(trajectory).y);
    UpdateIf(trajectory, true, {x:x - PlayScreen.Width / 100})
);

上記のようにすることにより、糸の点の情報が左方向にずれていくため、相対的に糸が右に進んでいるように見えるというわけです。

また、画面外にずれた情報は不要ですので削除しちゃいましょう。

これで糸のアニメーションを作成することができました!

リングを作成する

糸が通過するためのリングを作成したいと思います。

リングの情報は ring に持ちたいと思います。

このコレクションにはリングの座標と、リングの大きさを保持しています。

PlayScreen.OnVisible

ClearCollect(ring, {cx:PlayScreen.Width + 15, cy:PlayScreen.Height / 2, ry:50});

このコレクション情報をもとにリングを描画したいと思います。

Image.Image

' stroke='black' fill='transparent' stroke-width='2' />" & 
        Concat(
            ForAll(
                Sequence(CountRows(ring)),
                "<ellipse 
                    cx='" & Last(FirstN(ring, Value)).cx & "' 
                    cy='" & Last(FirstN(ring, Value)).cy & "' 
                    rx='15' 
                    ry='" & Last(FirstN(ring, Value)).ry & "' 
                stroke='red' fill='none' stroke-width='2' />"
            ),
            Value
        )

これで赤い楕円のリングを描画することができます。

続いて、糸の情報と同じように1msごとにコレクションの情報を更新したいと思います。

Timer.OnTimerStart

If(
    Last(ring).cx < PlayScreen.Width * 2 / 3,
    Collect(
        ring,
        {
            cx:PlayScreen.Width,
            cy:
                With(
                    {val:Last(ring).cy + Rand() * PlayScreen.Height * 2 / 9 - PlayScreen.Height * 2 / 18},
                    If(val > PlayScreen.Height - 150, 
                           PlayScreen.Height - 150,
                       val < 150,
                           150,
                       val
                    )
                ),
            ry:50 / (Rand() * 1 / 2 + 1)
        }
    )
)

Timer1.OnTimerEnd

UpdateIf(ring, true, {cx:cx - PlayScreen.Width / 100});
RemoveIf(ring, cx < -15);

リングは1msごとに作成する必要はないので、If 関数で作成する条件を制御しています。
今回、条件はリングが画面の2 / 3まできたら新規で追加するようにしています。

追加されたリングは1msごとに左にずれるようx座標を更新し、画面外に出た情報は削除するようにしています。

これでリング部分の実装が完成です!

糸がリングを通過したか判定を行う

糸がリングを通過したか判定を行うのには、トグルを利用します。

"切り替え"を追加して、以下のように設定します。

Toggle.Default

CountRows(
    Filter(
        ring,
        cx - 15 <= Last(trajectory).x,
        cx + 15 >= Last(trajectory).x,
        cy - ry > Last(trajectory).y ||
        cy + ry < Last(trajectory).y
    )
) <> 0

トグルが true になると、糸がリングを通過していないのでゲーム終了となります。

上記条件を簡単に解説します。

まず、判断を行うのは糸の先端ですので Last(trajectory) で糸の先端部分の座標を取得しています。
cx - 15 としているのは、リング横幅を15で固定しているためです。

x座標はリング内にいたときにいたときをフィルタリングしたのに対し、y座標はリング外にいたときでフィルタリングする必要があります。

y座標がリング外にいるというのは以下2つの場合が存在します。

  • リング上部より上にいるとき
  • リング下部より下にいるとき

書き出してみると当たり前じゃん。という感じですが、「どういったときにやりたいことを満たせる条件になるか」を考えてみると実装が少し楽になったりします。

1つ目の条件を満たしているのは、 cy - ry > Last(trajectory).y
2つ目の条件を満たしているのが、 cy + ry < Last(trajectory).y
です。

これでPower Apps での糸通しゲームの完成です!

おわりに

ご覧のとおり結構単純な実装で糸通しゲームが作成できました!

この技術を応用すれば横スクロールゲームも作成できそうですね。

長い文字列を変数に保存した際にPower Apps が落ちる?(検証中)

はじめに

Power Apps でアプリ作成中に文字列(150万文字程)を保存しようとしたところ下記のようにページが応答しなくなったので、色々試したみた記事になります。

ただし具体的な制限などはまだみつけられていません...
自身のメモ書き程度に記載された記事です。
具体的な結論はまだみつかっていません。
ご容赦ください。

f:id:koruneko:20201221002448p:plain

事象

Power Apps にて、150万文字程度の文字列をSet 関数を用いて変数に保存しようとしたところ下のように応答なしとなってしまった。

f:id:koruneko:20201221003448p:plain

(スクショミスって応答なしが含まれていませんが操作できなくなっています。)

この事象が、一体何文字から発生するのか?検証すべく10万文字単位で文字数を増やしてみたのですが、なんと170万文字を超えても保存できてしまったんですよね...

再現性のない事象はやめてほしいですね...

以下は10万文字単位で検証した動画ではないですが、150万文字程度の文字列の貼り付け、表示、及び保存を行ってときのPower Apps の挙動です。

動画からもわかる通り、一応応答は返ってきてはいますが、結構動作がもっさりしてしまっていますね。

もしかすると、応答なしとなる一歩手前な状態で、応答なしとなるかどうかはそのときのネットワークやPCの状態によるのかもしれません。

引き続き調査し、なにかわかりましたら追記したいと思います。


スポンサードリンク