unit MainFm;
(*
 * Copyright (c) 2015 - 2025 Dr. Yuriy Kotsarenko. All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in
 * compliance with the License. You may obtain a copy of the License at
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software distributed under the License is
 * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and limitations under the License.
 *)

// This example illustrates the following features:
//  * Drawing different shapes, including vector-based paths with fill and stroke using 2D canvas.

interface

{$INCLUDE PXT.Config.inc}

uses
{$IFDEF MSWINDOWS}
  PXT.Controls,
{$ELSE}
  OpenGLContext,
{$ENDIF}
  Graphics, Classes, SysUtils, Forms, Controls, Dialogs, PXT.Types, PXT.Graphics;

type
  TMainForm = class(TForm)
    procedure FormCreate(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
  private
    FRenderingControl: TWinControl;
    FDisplaySize: TPoint2i;
    FDisplayScale: Single;

    FDevice: TDevice;
    FSwapChain: TSwapChain;
    FCanvas: TCanvas;
    FTextRenderer: TTextRenderer;
    FPathBroker: TPathBroker;
    FCanvasBuffer: TCanvasBuffer;
    FCanvasBufferExtents: TFloatRect;

    procedure RenderingControlResize(Sender: TObject);
    procedure RenderingControlPaint(Sender: TObject);
    procedure ApplicationIdle(Sender: TObject; var Done: Boolean);
    procedure CreatePathFillAndStroke;
    procedure CreateRenderingControl;
  public
  end;

var
  MainForm: TMainForm;

implementation
{$R *.lfm}

uses
  PXT.Headers;

procedure TMainForm.FormCreate(Sender: TObject);
begin
  CreateRenderingControl;
  FDisplaySize := Point2i(ClientWidth, ClientHeight);
  FDisplayScale := PixelsPerInch / 96.0;
  try
  {$IFDEF MSWINDOWS}
    FDevice := TDevice.Create;
  {$ELSE}
    // On Linux, using TOpenGLContext control from LCL.
    FDevice := TDevice.CreateWrapped;
  {$ENDIF}

    FSwapChain := TSwapChain.Create(FDevice,
      {$IFDEF MSWINDOWS}TRenderingControl(FRenderingControl).WindowHandle{$ELSE}0{$ENDIF},
      FDisplaySize, TPixelFormat.RGBA8, TPixelFormat.Unknown, 8, False);

    FCanvas := TCanvas.Create(FDevice);
    FTextRenderer := TTextRenderer.Create(FCanvas, Point2i(256, 256), TPixelFormat.LA8);

    FPathBroker := TPathBroker.Create;
    FCanvasBuffer := TCanvasBuffer.Create;

    CreatePathFillAndStroke;
  except
    on E: EAfterwarpException do
    begin
      MessageDlg(E.Message, mtError, [mbOK], 0);
      Application.Terminate;
      Exit;
    end;
  end;
  Application.OnIdle := ApplicationIdle;
end;

procedure TMainForm.FormDestroy(Sender: TObject);
begin
  FCanvasBuffer.Free;
  FPathBroker.Free;
  FTextRenderer.Free;
  FCanvas.Free;
  FSwapChain.Free;
  FDevice.Free;
end;

procedure TMainForm.RenderingControlResize(Sender: TObject);
begin
  FDisplaySize := Point2i(FRenderingControl.Width, FRenderingControl.Height);
  FSwapChain.Resize(FDisplaySize);
  FRenderingControl.Invalidate;
end;

procedure TMainForm.RenderingControlPaint(Sender: TObject);
var
  LOmega, LKappa, LBufferScale: Single;
  LHoleAt, LHoleSize: TPoint2f;
  LFinalExtents: TFloatRect;
  LFontSettings: TFontParameters;
  LTechString: string;
  LTicks: Double;
begin
  FDisplayScale := PixelsPerInch / 96.0;

  // One tick is incremented 60 times per second.
  LTicks := GetSystemTicks * 60.0 / 1000000.0;

  FSwapChain.BeginScene;
  try
    FDevice.Clear([TClearLayer.Color], FloatColor($FF33444E));

    FCanvas.BeginScene;
    try
      FCanvas.ContextState := TCanvasContextState.FlatScene;

      // Draw Hexagon.
      FCanvas.Attributes := [TCanvasAttribute.Transform];
      FCanvas.Transform := TMatrix3x2f.Scale(50.0 * FDisplayScale) * TMatrix3x2f.Rotate(LTicks * 0.00371) *
        TMatrix3x2f.Translate(FDisplaySize.X * 0.2, FDisplaySize.Y * 0.25);

      FCanvas.Hexagon($000000FF, $FF28D7FF, $000000FF, $FF28D7FF, $000000FF, $FF28D7FF);

      // Draw Arc.
      LOmega := LTicks * 0.01892;
      LKappa := 1.25 * Pi + Sin(LTicks * 0.01241) * 0.5 * Pi;

      FCanvas.Attributes := [];
      FCanvas.Arc(Point2f(FDisplaySize.X * 0.5, FDisplaySize.Y * 0.25), Point2f(70.0, 50.0) * FDisplayScale,
        LOmega, LOmega + LKappa, 64, ColorRect($FF81E5A4, $FF009CFF, $FFFF2877, $FFFFFFFF));

      // Draw small Tape.
      LOmega := LTicks * 0.01134;
      LKappa := 1.25 * Pi + Sin(LTicks * 0.014751) * 0.5 * Pi;

      FCanvas.Tape(Point2f(FDisplaySize.X * 0.8, FDisplaySize.Y * 0.25), Point2f(25.0, 20.0) * FDisplayScale,
        Point2f(45.0, 40.0) * FDisplayScale, LOmega, LOmega + LKappa, 64,
        ColorRect($FF4F24FF, $FF0DFFAC, $FFFF982B, $FFFF427B));

      // Draw large Tape.
      LOmega := LTicks * 0.01721;
      LKappa := 1.25 * Pi + Sin(LTicks * 0.01042) * 0.5 * Pi;

      FCanvas.Tape(Point2f(FDisplaySize.X * 0.8, FDisplaySize.Y * 0.25), Point2f(50.0, 45.0) * FDisplayScale,
        Point2f(70.0, 65.0) * FDisplayScale, LOmega, LOmega + LKappa, 64,
        ColorPair($FF4F24FF, $FF0DFFAC), ColorPair($FFFF982B, $FF81E5A4), ColorPair($FF009CFF, $FFFF2877));

      // Draw Hole with smooth internal border (using Ribbon).
      LHoleAt := Point2f(
        FDisplaySize.X * 0.75 + Cos(LTicks * 0.00718) * FDisplaySize.X * 0.15,
        FDisplaySize.Y * 0.75 + Sin(LTicks * 0.00912) * FDisplaySize.Y * 0.15);

      LHoleSize := Point2f(40.0, 40.0) * FDisplayScale;

      FCanvas.RectWithHole(FloatRect(FDisplaySize.X * 0.5, FDisplaySize.Y * 0.5, FDisplaySize.X * 0.5,
        FDisplaySize.Y * 0.5), LHoleAt, LHoleSize, ColorPair($0033444E, $FFD5DEE4), 64);

      FCanvas.Ribbon(LHoleAt, LHoleSize * 0.75, LHoleSize, 64, ColorPair($00D5DEE4, $FFD5DEE4),
        ColorPair($00D5DEE4, $FFD5DEE4), ColorPair($00D5DEE4, $FFD5DEE4));

      // Draw a vector-based path, rescaling and moving it where it should appear.
      LBufferScale := FDisplayScale / 1.25;

      LFinalExtents := FloatRect(
        FCanvasBufferExtents.Left * LBufferScale, FCanvasBufferExtents.Top * LBufferScale,
        FCanvasBufferExtents.Width * LBufferScale, FCanvasBufferExtents.Height * LBufferScale);

      FCanvas.Attributes := [TCanvasAttribute.Transform];
      FCanvas.Transform := TMatrix3x2f.Scale(LBufferScale) * TMatrix3x2f.Translate(
        FDisplaySize.X * 0.25 - (LFinalExtents.Left + LFinalExtents.Right) * 0.5,
        FDisplaySize.Y * 0.75 - (LFinalExtents.Top + LFinalExtents.Bottom) * 0.5);

      FCanvas.Draw(FCanvasBuffer);

      // Text information.
      FCanvas.Attributes := [];
      FCanvas.SamplerState := TCanvasSamplerState.Create(TTextureFilter.Nearest, TTextureFilter.Nearest);
      FCanvas.ContextState := TCanvasContextState.FlatText;

      LFontSettings := TFontParameters.Create('Cambria', 16.0 * FDisplayScale, TFontWeight.SemiBold);
      LFontSettings.Effect.BorderType := TFontBorder.SemiHeavy;
      LFontSettings.Effect.BorderBrightness := 1.0;
      LFontSettings.Effect.FillBrightness := 0.0;
      LFontSettings.Effect.ShadowBrightness := 0.75;
      LFontSettings.Effect.ShadowOpacity := 1.0;
      FTextRenderer.FontParameters := LFontSettings;

      FTextRenderer.DrawAligned(Point2f(FDisplaySize.X * 0.5, FDisplaySize.Y * 0.25 - 75.0 * FDisplayScale),
        'Arc', ColorPair($FF3BFFE5, $FF00FF00), TTextAlignment.Middle, TTextAlignment.Ending);

      FTextRenderer.DrawAligned(Point2f(FDisplaySize.X * 0.2, FDisplaySize.Y * 0.25 - 75.0 * FDisplayScale),
        'Hexagon', ColorPair($FF5DD2FF, $FF3600FF), TTextAlignment.Middle, TTextAlignment.Ending);

      FTextRenderer.DrawAligned(Point2f(FDisplaySize.X * 0.8, FDisplaySize.Y * 0.25 - 75.0 * FDisplayScale),
        'Tapes', ColorPair($FFFFFAEA, $FFFF427B), TTextAlignment.Middle, TTextAlignment.Ending);

      FTextRenderer.DrawAligned(Point2f(FDisplaySize.X * 0.25, FDisplaySize.Y * 0.75 - 110.0 * FDisplayScale),
        'Path', ColorPair($FF3BFFE5, $FF00E0FF), TTextAlignment.Middle, TTextAlignment.Ending);

      FTextRenderer.DrawAligned(Point2f(FDisplaySize.X * 0.75, FDisplaySize.Y * 0.75 - 110.0 * FDisplayScale),
        'Hole in rectangle', ColorPair($FFB3F4FF, $FF4C82A9), TTextAlignment.Middle, TTextAlignment.Ending);

      // Draw technology string.
      LFontSettings := TFontParameters.Create('Segoe UI', 14.0 * FDisplayScale, TFontWeight.SemiBold);
      LFontSettings.Effect.BorderType := TFontBorder.SemiHeavy;
      LFontSettings.Effect.BorderBrightness := 1.0;
      LFontSettings.Effect.FillBrightness := 0.0;
      LFontSettings.Effect.ShadowBrightness := 0.75;
      LFontSettings.Effect.ShadowOpacity := 1.0;
      FTextRenderer.FontParameters := LFontSettings;

      if FDevice.Technology = TDeviceTechnology.Direct3D then
        LTechString := 'Direct3D (' + IntToStr(FDevice.TechVersion shr 8) + '.' +
          IntToStr((FDevice.TechVersion shr 4) and $0F) + '), features: '
      else
        LTechString := 'OpenGL (' + IntToStr(FDevice.TechVersion shr 8) + '.' +
          IntToStr((FDevice.TechVersion shr 4) and $0F) + '+), features: ';

      LTechString := LTechString + IntToStr(FDevice.TechFeatureVersion shr 8) + '.' +
        IntToStr((FDevice.TechFeatureVersion shr 4) and $0F);

      FTextRenderer.DrawAligned(Point2f(FDisplaySize.X * 0.5, FDisplaySize.Y - 2.0 * FDisplayScale),
        LTechString, $A0FFFFFF, TTextAlignment.Middle, TTextAlignment.Ending);
    finally
      FCanvas.EndScene;
    end;
  finally
    FSwapChain.EndScene;
  end;
{$IFNDEF MSWINDOWS}
  TOpenGLControl(FRenderingControl).SwapBuffers;
{$ENDIF}
end;

procedure TMainForm.ApplicationIdle(Sender: TObject; var Done: Boolean);
begin
  FRenderingControl.Invalidate;
  Done := False;
end;

procedure TMainForm.CreatePathFillAndStroke;
var
  LBuilder: TPathBuilder;
begin
  LBuilder := TPathBuilder.Create;
  try
    // Important notes: the resulting path must not self-intersect and the distance between each segment
    // must be bigger than the stroke thickness. Similarly, if there are any holes, these need to be
    // created using opposite winding (e.g. counter-clockwise vs clockwise) and must not self-intersect,
    // nor intersect the parent path.

    // Create a path using some Bezier curves.
    LBuilder.MoveTo(
      Point2f(0.0, 0.0));

    LBuilder.CurveTo(
      Point2f(-100.0, 0.0),
      Point2f(100.0, 300.0),
      Point2f(100.0, 200.0));

    LBuilder.SmoothCurveTo(
      Point2f(250.0, 150.0),
      Point2f(250.0, 50.0));

    LBuilder.SmoothCurveTo(
      Point2f(100.0, 0.0),
      Point2f(0.0, 0.0));

    LBuilder.ClosePath;

    // Uncomment the following to create a small hole inside previous path.
{
    LBuilder.MoveTo(Point2f(175.0, 50.0));
    LBuilder.LineTo(Point2f(75.0, 100.0));
    LBuilder.LineTo(Point2f(85.0, 35.0));
    LBuilder.ClosePath;
}
    // Convert this path into a buffer that can be drawn with canvas.
    FCanvasBuffer.Clear;

    // First, let's fill the path.
    FPathBroker.Fill(FCanvasBuffer, LBuilder.Elements, ColorRect($FFFFFF00, $FF00FFFF, $FFFFFFFF, $FFFF00FF));

    // Second, let's draw some stroke on top of it.
    FPathBroker.Stroke(FCanvasBuffer, LBuilder.Elements, 10.0, $FF202020, TPathJoint.Miter);

    // Calculate the resulting dimensions of the buffer.
    FCanvasBufferExtents := FCanvasBuffer.Extents;
  finally
    LBuilder.Free;
  end;
end;

procedure TMainForm.CreateRenderingControl;
begin
{$IFDEF MSWINDOWS}
  FRenderingControl := TRenderingControl.Create(nil);
  FRenderingControl.Parent := Self;
  FRenderingControl.Align := alClient;
  FRenderingControl.Width := 256;
  FRenderingControl.Height := 256;

  with FRenderingControl as TRenderingControl do
  begin
    OnPaint := RenderingControlPaint;
    OnResize := RenderingControlResize;
  end;
{$ELSE}
  FRenderingControl := TOpenGLControl.Create(nil);
  FRenderingControl.Parent := Self;
  FRenderingControl.Align := alClient;
  FRenderingControl.Width := 256;
  FRenderingControl.Height := 256;

  with FRenderingControl as TOpenGLControl do
  begin
    OpenGLMajorVersion := 3;
    OpenGLMinorVersion := 3;
    DepthBits := 0;
    MultiSampling := 8;
    OnPaint := RenderingControlPaint;
    OnResize := RenderingControlResize;

    MakeCurrent;
  end;
{$ENDIF}
end;

end.
