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

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

・定番のボールが反射するアプリ。
・中央から自動展開する「花火モード」を搭載。
・部屋を暗くして大画面で眺めると花火を楽しめます(?)
・B型はこちら https://qiita.com/inf102/items/680c123b7791dd6eb1d1
:::


・WQHD(2560x1440) 自作機(MPU Core I5 7600K,VGA RTX3060) WAIT2 (0.002Sec)
<iframe width="1280" height="720" src="https://www.youtube.com/embed/AJvqoAh-SWA" title="ボール反射 #skiasharp #CG #wpf" 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" >

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

- <B>[A]</B>....花火モード(アルファ値を可変させて花火風にしている)
- <B>[F]</B>....フルスクリーン　フルスクリーンにならない時はリサイズ可能な表示にした後[F]キー

- <B>[カーソル上]</B>....ボールを増やす
- <B>[カーソル下]</B>....ボールを減らす

- <B>[マウス左]</B>....表示リセット
- <B>[マウス右]</B>....終了

<font COLOR="blue" >

## 2.パラメータ説明
</FONT>

[WAIT値]
- 2を指定すると0.002秒に一回画面更新します
- 大きすぎるとカクカク動作になります.通常は2-100程度です.

[MAX_BALL値]
- カーソルUpキーで表示数を可変させるのでその最大値です

[CRENT_BALL値]
- 起動時に表示されるボール数
- MAX_BALL値より少ない値にする

<font COLOR="blue" >

## 3.備考
</FONT>

- タイトルバーには現在の表示ボール数、最大表示可能数等を表示
- 出し過ぎると閉じるボタンを押しても中々閉じない場合あり.ボールは動いているが操作不能になる
- WPFはDirect3Dで描画しますのでVGAカード(又はMPU内蔵)の負荷テストとしても利用可
- 安価なノートPCでも1000個は出ましたので高価なVGAカードを載せている場合はもっと可能だと思います。少しづつ増やしてみて下さい
- 終了しない場合はタスクマネージャで止めます
- SkiaSharp参考URL ->  https://qiita.com/inf102/items/e6cc6d8590b929f5e54e

<font COLOR="blue" >

## 4.WPF (C#) Source code

</FONT>

```c#
/////////////////////////////////////////////////////////////////////////////////
// BallField WPF+SkiaSharp DotnetFramework.  (c)inf102 S.H. 2024.
// 1.Cur[Up][Down]Key [A]Key=AutoMode [F]Key=FullScreen 
// 2.Mouse LButton=Restart RButton=Exit
/////////////////////////////////////////////////////////////////////////////////

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=200;      // 開始時ボール数
        const  int WAIT=2 ;             // 画面更新間隔(mmSec) 2以上
///////////////////////////////////////////////////////////////////

        static int cnt=0;
        static bool AUTO_ENABE=false;
        static bool FULL_SCREEN=false;

        // 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;            // 大きさ
        }
        static POINT_XY[] XYDATA= new POINT_XY[MAX_BALL];

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

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

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

        void TITLE_OUTPUT() {
            Title ="WPF+SkiaSharp BALL Demo/BALL=" +CRENT_BALL.ToString()+" (MAX="+MAX_BALL.ToString()+")/Using[Up],[Down]Key/[A]Key=AUTOMode/[F}Key=FullScreen/LButton=Restart,RButton=Exit";
        }

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

            Random rnd = new System.Random();
            for(int x = 0; x < CRENT_BALL; x++) {

                XYDATA[x].RAG = rnd.Next(1,360);
                if (XYDATA[x].RAG == 180 || XYDATA[x].RAG == 270 || XYDATA[x].RAG == 90 ) XYDATA[x].RAG = rnd.Next(1,360);

                XYDATA[x].START_X = CAX/2;
                XYDATA[x].START_Y = CAY/2;
                XYDATA[x].L = 1;

                XYDATA[x].R=rnd.Next(0,255);
                XYDATA[x].G=rnd.Next(0,255);
                XYDATA[x].B=rnd.Next(0,255);
                XYDATA[x].A=rnd.Next(130,255); // 始めの方の数字を減らすと透明が増える

                XYDATA[x].SPEED = rnd.Next(1,15);
                XYDATA[x].SIZE=rnd.Next (2,70);
            }
        }

        private async  void Window_Loaded(object sender, RoutedEventArgs e) {

            while(true){
            
                await Task.Delay(WAIT);
                // 自動モード
                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 (cnt >= 100){
                        cnt=0;
                        BallDataSet();
                    }
                    cnt++;
                }

                GetXY();

                // 全ボールに対して壁に衝突したか 衝突の場合は反射角等を再設定
                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;
                TITLE_OUTPUT();
                BallDataSet();
            }

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

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

            // 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();
                }
            }
        }

        // ボール描画
        void MainPaintSurface(object sender, SKPaintSurfaceEventArgs args){
            CAX=args.Info.Width;
            CAY=args.Info.Height;

            args.Surface.Canvas.Clear(SKColors.Black);
            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,(Byte)XYDATA[x].A);
                args.Surface.Canvas.DrawCircle (XYDATA[x].CUR_X,XYDATA[x].CUR_Y,XYDATA[x].SIZE,BALL_PaintSkia);
            }
        }
    }
}
```
<font COLOR="blue" >

## 5.WPF (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="1200" Width="1800"  WindowState="Maximized" 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>

```