CG Library SkiaSharp「ボールフィールド B型」

### 　　　　2D専用グラフィックスライブラリ SkiaSharpデモ　 

::: note
・赤は親、子は赤以外。親は多少大きく、動きは少し遅くしてある。
・親は全ボール数の30%程度に設定。パラメータで可変。
・親と子が接近すると線が引かれる。離れると切れる。
・親と親は線が引かれない。子は複数の親と線が引かれる。
・線の色はランダムまたは固定選択。
・線が引かれる距離、ボール総数等はキー操作で可変。
・A型はこちら https://qiita.com/inf102/items/e4c95ee481d4a232acbd

:::
<font COLOR="blue" >

#### 1.キー操作説明</FONT>

- <B><font COLOR="RED" >[A]</B>.......................通常モード、自動展開モード切り替え
- <B><font COLOR="RED" >[F]</B>.......................フルスクリーン フルスクリーンにならない時はリサイズ可能な
　　　　　 　  表示にした後[F]キーまたは起動後にFキーまたは起動直後にFキー

- <B><font COLOR="RED" >[カーソル上]</B>....ボールを増やす
- <B><font COLOR="RED" >[カーソル下]</B>....ボールを減らす
- <B><font COLOR="RED" >[PageUp]</B>..........線が引かれるまでの距離を長く
- <B><font COLOR="RED" >[PageDw]</B>.........線が引かれるまでの距離を短く
- <B><font COLOR="RED" >[R]</B>.......................線の色をランダム/固定色の切り替え
- <B><font COLOR="RED" >[マウス左]</B>........表示リセット
- <B><font COLOR="RED" >[マウス右]</B>........終了
<font COLOR="blue" >

#### 2.動画(2560x1440) YouTube</FONT>
<iframe width="560" height="315" src="https://www.youtube.com/embed/PsOuZVfnLns?si=YQaTyKeCgB83ir4E" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>

<font COLOR="blue" >

#### 3.コードについて</FONT>
- タイプAを改修しました。
適当にコメント入れたので仕組みはコード見れば分かると思います。
- 親が300個、子が700個の場合は、一つの親が700個の子の位置を算出。それを300回繰り返します。他にボールの位置計算で三角関数2回x1000個分。かなりの計算量ですがPC性能のおかげで動作してます。

- ユークリッド距離算出は三平方の定理を元にしている「2点間の距離計算式」です。
![keyword_article_c3_mat_5_image03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2821882/1abd413f-4db5-5e1e-1f6d-f0a12d5dac75.png)

https://www.try-it.jp/keyword_articles/26/

- ボールの位置によっては計算できない事が有ったので直前で補正かけてます。
- SkiaSharp参考URL -> https://qiita.com/inf102/items/e6cc6d8590b929f5e54e
<font COLOR="blue" >

#### 4.WPF Sourcecode</FONT>

```c#
/////////////////////////////////////////////////////////
// BallField TypeB / WPF+SkiaSharp DotnetFramework.
// (c)inf102 S.H. 2024. allrights reserved.
/////////////////////////////////////////////////////////

using SkiaSharp;
using SkiaSharp.Views.Desktop;
using System;
using System.Threading.Tasks;
using System.Windows;

namespace SkiaSharpSample {

    public partial class MainWindow : Window    {

///////////////////////////////////////////////////////////////////
        const  int MAX_BALL=1000;       // 最大ボール数
        static int CRENT_BALL=150;      // 開始時ボール数
        const  int WAIT=2 ;             // 画面更新間隔(mmSec) 2以上
        static  double DIST=13.0;       // 線を引く距離
///////////////////////////////////////////////////////////////////

        static bool AUTO_ENABE=false;
        static bool FULL_SCREEN=false;
        static bool RND_COLOR=true;

        static int T_CNT=0;
        static int P_CNT=0;
        static int C_CNT=0;

        // BALL DATA
        struct POINT_XY{
            public int START_X,START_Y; // 開始位置
            public int L;               // 開始位置からの移動距離
            public int RAG;             // 角度
            public int CUR_X,CUR_Y;     // 現在位置
            public int R,G,B,A;         // 透明度、色
            public int SPEED;           // 速度
            public int SIZE;            // 大きさ
            public int P_FLG;           // 0=child 1=parent
        }
        static POINT_XY[] XYDATA= new POINT_XY[MAX_BALL];

        SKPaint BALL_PaintSkia = new SKPaint{
            Style = SKPaintStyle.Fill,
            IsAntialias=true,
        };

        SKPaint LINE_PaintSkia = new SKPaint{
            Style = SKPaintStyle.Fill,
            IsAntialias=true,
            StrokeWidth =2,
            Color=new SKColor(0,255,0)
        };

        // キャンパスサイズ
        static int CAX,CAY;

        public MainWindow()        {
            InitializeComponent();
            TITLE_OUTPUT();
        }

        void TITLE_OUTPUT() {
            Title ="WPF+SkiaSharp BallDemo2/AllBall="+CRENT_BALL.ToString() +"(MAX="+MAX_BALL.ToString()+")/Parent="+P_CNT.ToString()+"/Child="+C_CNT.ToString()+"/DIST="+DIST.ToString()+"/R=RndColor/L_Button=Restart,R_Button=Exit";
        }

        // Ballデータ設定
        public void BallDataSet(){

            P_CNT=0;
            C_CNT=0;
            Random rnd = new System.Random();

            for(int x = 0; x < CRENT_BALL; x++) {
                XYDATA[x].RAG = rnd.Next(1,360);

                // parent　
                if (rnd.Next(0,5) == 1) { //0,5は0->4
                    P_CNT++;
                    XYDATA[x].P_FLG=1;
                    XYDATA[x].SPEED = rnd.Next(1,12);
                    XYDATA[x].SIZE=11;
                    XYDATA[x].R=255;
                    XYDATA[x].G=0;
                    XYDATA[x].B=0;
                }
                // child
                else {
                    C_CNT++;
                    XYDATA[x].P_FLG=0;
                    XYDATA[x].SPEED = rnd.Next(1,16);
                    XYDATA[x].SIZE=8;
                    XYDATA[x].R=rnd.Next(0,50);
                    XYDATA[x].G=rnd.Next(40,255);
                    XYDATA[x].B=rnd.Next(40,255);
                }

                XYDATA[x].START_X = CAX/2;
                XYDATA[x].START_Y = CAY/2;
                XYDATA[x].L = 1;
            }
            TITLE_OUTPUT();
        }

        private async  void Window_Loaded(object sender, RoutedEventArgse) {

            while(true){
                await Task.Delay(WAIT);

                // 全てのボールの次の表示位置算出
                GetXY();

                // 自動展開モード
                if (AUTO_ENABE == true) {

                    for(int ff = 0; ff < CRENT_BALL; ff++) {
                        XYDATA[ff].A-=2;
                        if (XYDATA[ff].A<=0) XYDATA[ff].A=0;
                    }

                    if (T_CNT>= 130){
                        T_CNT=0;
                        BallDataSet();
                    }
                    T_CNT++;
                }

                // 全ボールに対して壁衝突判定、衝突の場合は反射角等を再設定
                for (int x=0;x<CRENT_BALL;x++){
                    if (AUTO_ENABE == false){ // 自動モードは反射させない
                        if (XYDATA[x].CUR_Y < XYDATA[x].SIZE){
                            XYDATA[x].L=1;
                            XYDATA[x].RAG=180-XYDATA[x].RAG;
                            XYDATA[x].START_Y=XYDATA[x].CUR_Y+3;
                            XYDATA[x].START_X=XYDATA[x].CUR_X;
                        }
                    }

                    // 下
                    if (AUTO_ENABE == false){ // 自動モードは反射させない
                        if (XYDATA[x].CUR_Y> CAY-XYDATA[x].SIZE){
                            XYDATA[x].L=1;
                            XYDATA[x].RAG=180-XYDATA[x].RAG;
                            XYDATA[x].START_Y=XYDATA[x].CUR_Y;
                            XYDATA[x].START_X=XYDATA[x].CUR_X;
                        }
                    }

                    // 右
                    if (AUTO_ENABE == false){ // 自動モードは反射させない
                        if (XYDATA[x].CUR_X>CAX-XYDATA[x].SIZE){
                            XYDATA[x].L=1;
                            XYDATA[x].RAG=360-XYDATA[x].RAG;
                            XYDATA[x].START_Y=XYDATA[x].CUR_Y;
                            XYDATA[x].START_X=XYDATA[x].CUR_X;
                        }
                    }

                    // 左
                    if (AUTO_ENABE == false){ // 自動モードは反射させない
                        if (XYDATA[x].CUR_X<XYDATA[x].SIZE){
                            XYDATA[x].L=1;
                            XYDATA[x].RAG=360-XYDATA[x].RAG;
                            XYDATA[x].START_Y=XYDATA[x].CUR_Y;
                            XYDATA[x].START_X=XYDATA[x].CUR_X+3;
                        }
                    }

                    // 移動速度
                    XYDATA[x].L+=XYDATA[x].SPEED;
                }

                // 画面更新
                MainCanvas.InvalidateVisual();
            }
        }

        // 全てのボールの次の表示位置算出
        void GetXY (){
            for (int x=0;x<CRENT_BALL;x++){
                XYDATA[x].CUR_X= (int)(XYDATA[x].START_X+XYDATA[x].L*Math.Sin(XYDATA[x].RAG*Math.PI/180));
                XYDATA[x].CUR_Y= (int)(XYDATA[x].START_Y-XYDATA[x].L*Math.Cos(XYDATA[x].RAG*Math.PI/180));
            }
        }

        private void skiaCanvas_SizeChanged(object sender,SizeChangedEventArgs e) {
            BallDataSet();
        }

        private void MainCanvas_PreviewMouseLeftButtonDown(object sender,System.Windows.Input.MouseButtonEventArgs e) {
            BallDataSet();
        }

        private void MainCanvas_PreviewMouseRightButtonDown(object sender,System.Windows.Input.MouseButtonEventArgs e) {
            Application.Current.Shutdown();
        }

        private void Window_KeyDown(object sender,System.Windows.Input.KeyEventArgs e) {

            if (e.Key == System.Windows.Input.Key.Up) {
                CRENT_BALL+=15;
                if (CRENT_BALL >=MAX_BALL ) CRENT_BALL=MAX_BALL;
                BallDataSet();
            }

            if (e.Key == System.Windows.Input.Key.Down) {
                CRENT_BALL-=15;
                if (CRENT_BALL <=1 ) CRENT_BALL=1;
                BallDataSet();
            }

            if (e.Key == System.Windows.Input.Key.PageUp) {
                DIST+=1.0f;
                if (DIST >=20.0f ) DIST=20.0f;
                TITLE_OUTPUT();
            }

            if (e.Key == System.Windows.Input.Key.PageDown) {
                DIST-=1.0f;
                if (DIST <=10.0f ) DIST=10.0f;
                TITLE_OUTPUT();
            }

            if (e.Key == System.Windows.Input.Key.R) {
                RND_COLOR=!RND_COLOR;
            }

            // FULL SCREEN
            if (e.Key == System.Windows.Input.Key.F) {
                if (FULL_SCREEN == false){
                    FULL_SCREEN=!FULL_SCREEN;
                    this.WindowStyle = WindowStyle.None; //タイトル消す場合
                    this.WindowState = WindowState.Maximized;
                    BallDataSet();
                    return;
                }
                else {
                    FULL_SCREEN=!FULL_SCREEN;
                    this.WindowStyle = WindowStyle.SingleBorderWindow;
                    BallDataSet();
                }
            }

            // 表示モード切り替え
            if (e.Key == System.Windows.Input.Key.A) {
                AUTO_ENABE=!AUTO_ENABE;
                BallDataSet();
            }
        }

        // ボール、線描画
        void MainPaintSurface(object sender, SKPaintSurfaceEventArgs args){

            CAX=args.Info.Width;
            CAY=args.Info.Height;
            args.Surface.Canvas.Clear(SKColors.Black);

            double L;
            int x0,y0,x1,y1;
            Random rnd = new System.Random();

            // 全ボール描画
            for (int x=0;x<CRENT_BALL;x++) {

                BALL_PaintSkia.Color=new SKColor((byte)XYDATA[x].R,(byte)XYDATA[x].G,(byte)XYDATA[x].B);
                args.Surface.Canvas.DrawCircle (XYDATA[x].CUR_X,XYDATA[x].CUR_Y,XYDATA[x].SIZE,BALL_PaintSkia);

                // 親
                if (XYDATA[x].P_FLG == 1) {

                    // 全ての子と距離算出
                    for(int f = 0; f < CRENT_BALL; f++) {

                        // 子
                        if(XYDATA[f].P_FLG == 0) {

                            // 距離算出用補正
                            if (XYDATA[x].CUR_X - XYDATA[f].CUR_X < 0) {
                                x0 = XYDATA[f].CUR_X;
                                x1 = XYDATA[x].CUR_X;
                            }
                            else {
                                x0 = XYDATA[x].CUR_X;
                                x1 = XYDATA[f].CUR_X;
                            }

                            if (XYDATA[x].CUR_Y - XYDATA[f].CUR_Y < 0) {
                                y0 = XYDATA[f].CUR_Y;
                                y1 = XYDATA[x].CUR_Y;
                            }
                            else {
                                y0= XYDATA[x].CUR_Y;
                                y1= XYDATA[f].CUR_Y;
                            }

                            // 距離算出
                            L = Math.Sqrt (((x0-x1)^2)+((y0-y1)^2));

                            // LINE_COLOR
                            if (RND_COLOR==true) LINE_PaintSkia.Color=new SKColor((byte)rnd.Next(70,255),(byte)rnd.Next(70,255),(byte)rnd.Next(70,255));
                            else  LINE_PaintSkia.Color=new SKColor((byte)0,(byte)255,(byte)0);

                            // 親と子の線
                            if (L < DIST ) { 
                                args.Surface.Canvas.DrawLine(XYDATA[x].CUR_X,XYDATA[x].CUR_Y,XYDATA[f].CUR_X,XYDATA[f].CUR_Y,LINE_PaintSkia);
                            }
                        }
                    }
                }
            }
        }
    }
}
```

<font COLOR="blue" >

#### 5.XAML</FONT>

```xml
<Window x:Class="SkiaSharpSample.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:SkiaSharpSample" xmlns:skia="clr-namespace:SkiaSharp.Views.WPF;assembly=SkiaSharp.Views.WPF" 
        xmlns:wpf="clr-namespace:SkiaSharp.Views.WPF;assembly=SkiaSharp.Views.WPF" mc:Ignorable="d"
        Title="WPF+SkiaSharp BALLDemo" Loaded="Window_Loaded" Height="640" Width="900"  KeyDown="Window_KeyDown" >

    <Grid>
    	<wpf:SKElement x:Name="MainCanvas" PaintSurface="MainPaintSurface" Grid.Column="0" Grid.Row="0" SizeChanged="skiaCanvas_SizeChanged" PreviewMouseLeftButtonDown="MainCanvas_PreviewMouseLeftButtonDown" PreviewMouseRightButtonDown="MainCanvas_PreviewMouseRightButtonDown" />
    </Grid>
</Window>
```