CG Library SkiaSharp「正弦波形ジェネレータ」

<B>
<font COLOR="blue" >
・----------------------------------------------------------------------------------・<BR>
・sinカーブ(y=sin x)の動画を見て面白そうなので同様な物を作ってみました。<BR>
・x軸間隔、更新間隔等変更可能で色々な正弦波形を楽しむ事ができます。<br>
・SkiaSharpについては<a href="https://qiita.com/inf102/items/e6cc6d8590b929f5e54e">別記事参照</A><BR>
・-----------------------------------------------------------------------------------・<BR>
</B>

<FONT COLOR="BLUE">

#### 1.稼働画面

</FONT>


![B - コピー (6).jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2821882/dcfe45a5-e808-7b94-4d4b-8bbc9ab863b1.jpeg)

<iframe width="560" height="315" src="https://www.youtube.com/embed/I-KtUzqZkHE?si=_8FmWbkCJYzwr2WO" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>
サインカーブが右端に行った後スムーズに横スクロール開始させるのに少し手間取りました。<BR>
<HR>
<FONT COLOR="BLUE">

#### 2.Grid分割

</FONT>

非同期描画させる為Gridは1-3に分割しそれぞれ文字回転、円、sinグラフ用にSKElementの指定をします。(2,3は分ける必要は無かったかも知れない)

![B - コピー (5).jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2821882/88961f5e-4b59-e593-728c-3b992cd43ae1.jpeg)

```xml
<wpf:SKElement x:Name="Circle" PaintSurface="CirclePaintSurface" Grid.Row="1" Grid.Column="0"/>
<wpf:SKElement x:Name="Data" PaintSurface="DataPaintSurface" Grid.Row="1" Grid.Column="1" />
<wpf:SKElement x:Name="Rotation" PaintSurface="RotationPaintSurface"  Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2" />

```     

<HR>

<FONT COLOR="BLUE">

#### 3.円(半径線)とsinカーブ描画 呼び出し
</FONT>

```c#
while (true){
             
    // 稼働中
    if ((string)START.Content == "停止"){
        Circle.InvalidateVisual(); 
        Data.InvalidateVisual(); 
        
        await Task.Delay((int)INTER_SLI.Value);
               
        if (cnt >=720 ) cnt =0;
        cnt+=1;
        continue;
    }

    await Task.Delay(100);
}

```



<HR>

<FONT COLOR="BLUE">

#### 4.Y軸補助線　

</FONT>



見かけ上は分からないが、円描画グリッドとsinカーブ描画グリッドで分割しているので2本のＹ軸補助線を引く必要がある。
赤字1は円描画グリッドで描画、2はsinカーブ描画グリッド内で描画させる事によりY軸補助線が完成する。
動作中によくみるとタイミングの関係上、1,2の境で少しズレがあるのが確認できる。

![B - コピー (9).jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2821882/26e06d35-e498-63f1-69c5-acb46a37e059.jpeg)


<HR>
<FONT COLOR="BLUE">

#### 5.コード概要
</FONT>



* sinカーブ描画は座標データを構造体に格納。
(X0～X510までの511ポイントの位置)
画面をクリアした後、構造体に格納したXY値を読みだして初めから描画を繰り返しています。そうしないとY軸補助線が残ったままになってしまいます。

* グラフが右端まで行くとスクロールを開始しますが表示消し->データシフト->描画を繰り返しています。一般的にグラフィックスは描画より消す操作の方が大変です。
* 改修すれば矩形波、鋸歯状波等も表示できます。
* X軸間隔を変えるとグラフが崩れますが暫くすると直ります。
(後日分かった事で描画データの消去についてcanvasに分けて描画すればそんなに苦労しないらしい)

<HR>

<FONT COLOR="BLUE">

#### 6.WPF (C#) ソースコード (文字回転はM.S社のサンプルを改変)

</FONT>


```c#
///////////////////////////////////////////////
// 正弦波形ジェネレータ WPF+SkiaSharp
// (c)inf102 2023.
///////////////////////////////////////////////
///
using SkiaSharp;
using SkiaSharp.Views.Desktop;
using System;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;

namespace SkiaSharpSample {

    public partial class MainWindow : Window	{

        static int cnt;
        static int[] hy= new int[15000];
        static int y2;
        static int xycnt;
        static double xy3;
        static double BOR=1;
        static int cnt5;

        // 回転
        static int r=0;
   
        // サークル内Y値表示
        static int YFLG=0;

        // 表示データ
        struct RcvData{
            public int x,y;
        }
        // 表示データ構造体配列
        static RcvData[] XY = new RcvData[15000];
    
        // グラフ部波線データ
        static readonly float[] dashArray = new float[4];
    
		public MainWindow()		{
			InitializeComponent();
            
            // グラフ部波線データ
            dashArray[0] = 3.5f;  //0.21 2.5 2.5
            dashArray[1] = 2.0f;

		}

        private async void Window_Loaded(object sender, RoutedEventArgs e) {
         
            // 文字回転
            _=Task.Run(()=>{
                while (true){
                    
                    r++;
                    this.Dispatcher.Invoke((Action)(() =>{
                        Rotation.InvalidateVisual(); 
                           
                    }));

                    SpinWait.SpinUntil(()=> false, 1000 / 510);
                }

            });


            while (true){

                // 稼働中
                if ((string)START.Content == "停止"){
                    Circle.InvalidateVisual(); 
                    Data.InvalidateVisual(); 
                    
                    await Task.Delay((int)INTER_SLI.Value);
               
                    if (cnt >=720 ) cnt =0;
                 
                    cnt+=1;
                    continue;
                }
               
                await Task.Delay(1);
              //  SpinWait.SpinUntil(()=> false, 1000 / 100);
            }

        }

		void CirclePaintSurface(object sender, SKPaintSurfaceEventArgs args){

            SKSurface surface = args.Surface;
            SKCanvas canvas = surface.Canvas; 

            canvas.Clear(new SKColor((byte)125, (byte)107, (byte)195));

            ///////////////////////////////////////////////////////////////////
            
            // 円
            var skPaint = new SKPaint() {
	            Style =  SKPaintStyle.Stroke,
			    Color = new SKColor(0, 191, 200),
                IsAntialias=true,
                StrokeWidth = 3,

            };

            // サークル内X補助線
            var X_LINEskPaint = new SKPaint() {
	            Style =  SKPaintStyle.Stroke,
			    Color = new SKColor(0, 191, 200),
                IsAntialias=true,
                StrokeWidth = 2,

            };

            // サークル内XY線
            var X_LINE2skPaint = new SKPaint() {
	            Style =  SKPaintStyle.Stroke,
			    Color = new SKColor(255, 0, 00),
                IsAntialias=true,
                StrokeWidth = (float)2.5,

            };
            
            // Y値数値表示
            var Y_TEXYskPaint = new SKPaint() {
                FilterQuality = SKFilterQuality.High,
                LcdRenderText=true,
                IsAntialias = true,
                TextSize = 15f,
                Color = SKColors.White,
                Typeface = SKTypeface.FromFamilyName("LINE Seed JP_OTF")
            };


            //////////////////////////////////////////////////////////////////////////
            // 数値文字 1,-1
            var TextskPaint = new SKPaint() {
                FilterQuality = SKFilterQuality.High,
                LcdRenderText=true,
                IsAntialias = true,
                TextSize = 14f,
                Color = SKColors.White,
                Typeface = SKTypeface.FromFamilyName("LINE Seed JP_OTF")
            };

            args.Surface.Canvas.DrawText("1", 95, 18, TextskPaint);
            args.Surface.Canvas.DrawText("-1", 90, 215, TextskPaint);
            args.Surface.Canvas.DrawText("0", 95, 126, TextskPaint);
            args.Surface.Canvas.DrawText("-1", 5, 125, TextskPaint);
            args.Surface.Canvas.DrawText("1", 200, 125, TextskPaint);
            //////////////////////////////////////////////////////////////////////////
            
            ///////////////////////////////////////////////////////////////////

            // サークル本体
            canvas.DrawCircle((float)110,(float)110,89,skPaint);
            
            // XY線(十字線)
            canvas.DrawLine ((float)10,(float)110,(float)210,(float)110,X_LINE2skPaint);
            canvas.DrawLine ((float)110,(float)10,(float)110,(float)210,X_LINE2skPaint);

            // 周る半径の線
            double x=110+87*Math.Sin (cnt*3.14/360);
            double y=110-87*Math.Cos (cnt*3.14/360);
            canvas.DrawLine ( (float)110,(float)110,(float)x,(float)y,skPaint);

            // サークル内X補助線
            if (X_LINE.IsChecked==true) canvas.DrawLine ((float)x,(float)y,(float)250,(float)y,X_LINEskPaint);


            //////////////////////////////////////////////////////////////////////
            // サークル内 Y値(文字)数値表示
            double fv=0;
            string dd="";
                       
            if ( YFLG ==0 ) fv=1-(cnt*0.005555555555);
            if ( 1-(cnt*0.005555555555) <= -1.01 ) YFLG = 1;
            if ( YFLG == 1 ) fv=(cnt*0.005555555555)-3;
            if ( YFLG == 1 && ((cnt*0.005555555555)-3) >=0.99999 ) YFLG=0;
            
            dd=fv.ToString();
            if (dd.Length >=3 ) dd=fv.ToString().Substring(0, 5);

            if (dd=="-3.00" ) dd="0";
            if (dd=="-0.00" ) dd="0";
            if (dd=="1.000" ) dd="0";
            
            args.Surface.Canvas.DrawText("y="+ dd   , 30, 100, Y_TEXYskPaint);
         
            //////////////////////////////////////////////////////////////////////

            y2=(int)y;

            if (xycnt<=10000) hy[xycnt++]=y2;

		}



        // サインカーブ描画
		void DataPaintSurface(object sender, SKPaintSurfaceEventArgs args){

            SKSurface surface = args.Surface;
            SKCanvas canvas = surface.Canvas;

            // グラフ部背景色
            canvas.Clear (new SKColor((byte)BR_SLI.Value, (byte)BG_SLI.Value, (byte)BB_SLI.Value));

            ///////////////////////////////////////////////////////////////////

            // sinData
            var DataskPaint = new SKPaint() {
                LcdRenderText=true,
	            Color = new SKColor((byte)R_SLI.Value, (byte)G_SLI.Value, (byte)B_SLI.Value),
                IsAntialias=true,
                StrokeWidth = (float)BDR_SLI.Value
            };
              
            // Y軸補助線
            var X_LINEskPaint = new SKPaint() {
	            Style =  SKPaintStyle.Stroke,
			    Color = new SKColor(0, 191, 200),
                IsAntialias=true,
                StrokeWidth = 2,
            };

            // CENTER LINE
            SKPaint CENTER_LINE1= new SKPaint{
	            Style =  SKPaintStyle.StrokeAndFill,
                Color = new SKColor(0, 00, 255),
                IsAntialias=true,
                PathEffect = SKPathEffect.CreateDash(dashArray, 10),
                StrokeWidth =(float)2.1

            };

            // Up/DwLINE
            SKPaint UpDw_LINE= new SKPaint{
	            Style =  SKPaintStyle.StrokeAndFill,
                Color = new SKColor(0, 220, 000),
                IsAntialias=true,
                PathEffect = SKPathEffect.CreateDash(dashArray, 10),
                StrokeWidth =1

            };

            // 罫線
            SKPaint XY_LINE= new SKPaint{
	            Style =  SKPaintStyle.StrokeAndFill,
                Color = new SKColor(254, 200, 200),
                IsAntialias=true,
                PathEffect = SKPathEffect.CreateDash(dashArray, 10),
                StrokeWidth =1

            };

            // CENTER LINE
            canvas.DrawLine ((float)0,(float)110,(float)510,(float)110,CENTER_LINE1);

            // Up_LINE
            canvas.DrawLine ((float)0,(float)22,(float)510,(float)22,UpDw_LINE);
            
            // Dw_LINE
            canvas.DrawLine ((float)0,(float)198,(float)510,(float)198,UpDw_LINE);

            // sin RGB
            R1TEXT.Text=((int)R_SLI.Value).ToString();
            G1TEXT.Text=((int)G_SLI.Value).ToString();
            B1TEXT.Text=((int)B_SLI.Value).ToString();

            BR1TEXT.Text=((int)BR_SLI.Value).ToString();
            BG1TEXT.Text=((int)BG_SLI.Value).ToString();
            BB1TEXT.Text=((int)BB_SLI.Value).ToString();

            // X幅
            if (X_SLI.Value.ToString().Length >= 3) X_TEXT.Text=X_SLI.Value.ToString().Substring(0, 3);
                      
            // Sinデータ線太さ
            BDRTEXT.Text=((float)BDR_SLI.Value).ToString();

            /////////////////////////////////////////////////////////////////////
            double X_RATE=BOR;      //0.27;   // X軸増加ドット数
            int    X_REMIT=510;     // グラフのX最大幅 これを超えるとスクロール開始

            double XMAX=1/X_RATE*X_REMIT;

            if ( xy3 <= X_REMIT ){
                xy3+=X_RATE;

                XY[cnt5++].x=(int)xy3;
                XY[cnt5].y=y2;

                for (int g=0;g<cnt5;g++){

                    // グラフ
                    canvas.DrawPoint((int)XY[g].x,XY[g].y,DataskPaint);
                    
                    // X補助線
                    if (X_LINE.IsChecked == true) canvas.DrawLine ((float)0,(float)y2,(float)XY[g].x,(float)y2,X_LINEskPaint);
                }
               
            }
            else {
     
                hy[(int)XMAX]=y2;
                xycnt=0;
                
                for (int tt=0;tt<=(int)XMAX;tt++){
                    hy[tt]=hy[tt+1];
                }

                double vv=0;

                // グラフ
                for(int  nc = 0; nc < (int)XMAX; nc+=1) {
                    vv+=X_RATE;
                    canvas.DrawPoint((int)vv,hy[nc],DataskPaint);
                    
                }

                // X補助線
                if (X_LINE.IsChecked==true) canvas.DrawLine ((float)0,(float)y2,(float)510,(float)y2,X_LINEskPaint);
            }

          
            // 罫線
            if (XYLINE.IsChecked == true) {

                // X
                for(int g = 00; g < 510; g += (int)20) {
                   args.Surface.Canvas.DrawLine(g, 20, g, 200, XY_LINE);
                }
  
                // Y
                for(int g = 20; g <210; g += (int)20) {
                    args.Surface.Canvas.DrawLine(0, g, 510, g, XY_LINE);
                }
            }

        }    

        // X軸幅
        private void R_SLIA_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e) {
            BOR=X_SLI.Value;
        }

        // 開始停止ボタン
        private void Button_Click(object sender, RoutedEventArgs e) {
            if ((string)START.Content == "開始") START.Content="停止";
            
            else START.Content="開始";
        }

        // 回転
        // https://learn.microsoft.com/ja-jp/xamarin/xamarin-forms/user-interface/graphics/skiasharp/transforms/3d-rotation
        void RotationPaintSurface(object sender, SKPaintSurfaceEventArgs args){

            SKSurface surface = args.Surface;
            SKCanvas canvas = surface.Canvas;

            canvas.Clear(SKColors.Black);

            SKPaint textPaint = new SKPaint{
                Style = SKPaintStyle.Fill,
                IsAntialias=true,
                LcdRenderText=true,
                TextSize = 9,
                TextAlign= (SKTextAlign)SKTextAlign.Center,
                Typeface = SKTypeface.FromFamilyName("LINE Seed JP_OTF"),
            };

            textPaint.Color = new SKColor(0, 100,205);

            // Find center of canvas
            float xCenter = (float)this.Width / 2;
            float yCenter = 7;

            // Translate center to origin
            SKMatrix matrix = SKMatrix.MakeTranslation(-xCenter, -yCenter);

            // Scale so text fits
            float scale = Math.Min(3,3);
            SKMatrix.PostConcat(ref matrix, SKMatrix.MakeScale(scale, scale));

            // Calculate composite 3D transforms
            float depth = 0.75f * scale * 100;
            SKMatrix44 matrix44 = SKMatrix44.CreateIdentity();
            matrix44.PostConcat(SKMatrix44.CreateRotationDegrees(0, 1, 0, (float)r));   
            SKMatrix44 perspectiveMatrix = SKMatrix44.CreateIdentity();
            perspectiveMatrix[3, 2] = -1 / depth;
            matrix44.PostConcat(perspectiveMatrix);

            // Concatenate with 2D matrix
            SKMatrix.PostConcat(ref matrix, matrix44.Matrix);

            // Translate back to center
            SKMatrix.PostConcat(ref matrix,SKMatrix.MakeTranslation(xCenter, yCenter));

            // Set the matrix and display the text
            canvas.SetMatrix(matrix);
            float xText = xCenter - 0;  
            float yText = yCenter + 7;

            canvas.DrawText("正弦波形ジェネレータ", xText, yText+1, textPaint);
        }
	}
}
```
<font color="blue">

#### 7.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:wpf="clr-namespace:SkiaSharp.Views.WPF;assembly=SkiaSharp.Views.WPF" mc:Ignorable="d"
        Title="SinCurve Generator" Height="360" Width="750" Icon="icon.ico" Loaded="Window_Loaded" ResizeMode="NoResize">

    <Grid Margin="1,1,1,1" >
        <Grid.RowDefinitions>
            <RowDefinition Height="40"/>
            <RowDefinition Height="220"/>
            <RowDefinition Height="62"/>
        </Grid.RowDefinitions>

        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="220"/>
            <ColumnDefinition Width="*"/>

        </Grid.ColumnDefinitions>

        <StackPanel Orientation="Horizontal" Grid.ColumnSpan="2"  Grid.Row="2" Grid.Column="0" Background="#FF5A9EEA" Height="60" VerticalAlignment="Top" >

            <StackPanel Margin="4,1,1,1" Orientation="Vertical" Height="45" >

                <StackPanel Orientation="Horizontal" >
                    <TextBlock Text="　           Ｒ" />
                    <Slider x:Name="R_SLI"  Width="80" Height="14"  Maximum="254" SmallChange="1" Cursor="SizeWE"/>
                    <TextBlock x:Name="R1TEXT" Text="DAT" />
                </StackPanel>

                <StackPanel Orientation="Horizontal" Margin="0,0,0,0">
                    <TextBlock Text="ﾃﾞｰﾀ線色Ｇ" />
                    <Slider x:Name="G_SLI" Width="80" Height="14"  Maximum="254" Value="254" SmallChange="1" Cursor="SizeWE"/>
                    <TextBlock  x:Name="G1TEXT" Text="DAT" />
                </StackPanel>

                <StackPanel Orientation="Horizontal" >
                    <TextBlock Text="　           Ｂ" />
                    <Slider x:Name="B_SLI" Width="80" Height="14"  Maximum="254" SmallChange="1" Cursor="SizeWE"/>
                    <TextBlock  x:Name="B1TEXT" Text="DAT" />
                </StackPanel>

            </StackPanel>

            <StackPanel Margin="4,1,1,1" Orientation="Vertical" Height="45" >

                <StackPanel Orientation="Horizontal" >
                    <TextBlock Text="　       Ｒ" />
                    <Slider x:Name="BR_SLI"  Width="80" Height="14"  Maximum="254" SmallChange="1" Cursor="SizeWE" Value="125" />
                    <TextBlock x:Name="BR1TEXT" Text="DAT" />
                </StackPanel>

                <StackPanel Orientation="Horizontal" Margin="0,0,0,0">
                    <TextBlock Text="背景色Ｇ" />
                    <Slider x:Name="BG_SLI" Width="80" Height="14"  Maximum="254"  SmallChange="1" Cursor="SizeWE" Value="107"/>
                    <TextBlock  x:Name="BG1TEXT" Text="DAT" />
                </StackPanel>

                <StackPanel Orientation="Horizontal" >
                    <TextBlock Text="　       Ｂ" />
                    <Slider x:Name="BB_SLI" Width="80" Height="14"  Maximum="254" SmallChange="1" Cursor="SizeWE" Value="195" />
                    <TextBlock  x:Name="BB1TEXT" Text="DAT" />
                </StackPanel>

            </StackPanel>
            <StackPanel Margin="4,1,1,1" Orientation="Horizontal" Height="45" >

                <StackPanel Orientation="Vertical" >
                    <TextBlock Text="X軸間隔 " Height="16" Width="45" />
                    <Slider x:Name="X_SLI"  Width="69" Height="14"  Maximum="1.5" Cursor="SizeWE" TickFrequency="1.2" Minimum="0.2" Value="0.2" ValueChanged="R_SLIA_ValueChanged"  />
                    <TextBlock  x:Name="X_TEXT" Text="DAT" Height="14" />
                </StackPanel>
    
            </StackPanel>

            <StackPanel Margin="4,1,1,1" Orientation="Horizontal" Height="45" >

                <StackPanel Orientation="Vertical" >
                    <TextBlock Text="太さ " Height="16" Width="24" />
                    <Slider x:Name="BDR_SLI"  Width="69" Height="14"  Maximum="5" Cursor="SizeWE" TickFrequency="1" Minimum="1" Value="2.4" />
                    <TextBlock  x:Name="BDRTEXT" Text="DAT" Height="14" />
                </StackPanel>

            </StackPanel>

            <StackPanel Margin="4,1,1,1" Orientation="Vertical" Height="45" >

                <StackPanel Orientation="Vertical" >
                    <TextBlock Text="更新間隔" Height="16" Width="53" />
                    <Slider x:Name="INTER_SLI"  Width="69" Height="14"  Maximum="155" Cursor="SizeWE" TickFrequency="1" Minimum="1" Value="1" />
                    <TextBlock Text="(速)        (遅)" Height="16" />
                </StackPanel>

            </StackPanel>

            <StackPanel Margin="4,1,1,1" Orientation="Horizontal" Height="45" >

                <StackPanel Orientation="Horizontal" >
                    <TextBlock Text="X軸補助線 " Height="14" />
                    <CheckBox x:Name="X_LINE" Height="15" IsChecked="True" ></CheckBox>
                
                </StackPanel>

            </StackPanel>

            <StackPanel Margin="4,1,1,1" Orientation="Horizontal" Height="45" >

                <StackPanel Orientation="Horizontal" >
                    <TextBlock Text="罫線" Height="14" />
                    <CheckBox x:Name="XYLINE" Height="15" IsChecked="false" ></CheckBox>

                </StackPanel>

            </StackPanel>

            <StackPanel Margin="4,1,1,1" Orientation="Horizontal" Height="45" >

                <StackPanel Orientation="Horizontal" Height="28" >
                    <Button x:Name="START" Content="停止" Height="24" Width="45" Click="Button_Click"/>
                   

                </StackPanel>

            </StackPanel>
        </StackPanel>

        <wpf:SKElement x:Name="Circle" PaintSurface="CirclePaintSurface" Grid.Row="1" Grid.Column="0"/>
        <wpf:SKElement x:Name="Data" PaintSurface="DataPaintSurface" Grid.Row="1" Grid.Column="1" />
        <wpf:SKElement x:Name="Rotation" PaintSurface="RotationPaintSurface"  Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2" />

    </Grid>
</Window>

```