Vala プログラミング

WebGPU プログラミング

おなが@京都先端科学大

Swift CairoGraphics

前回報告した GNUstep CoreGraphics は、「今更 GNUstep ?」感がありますので、同様な事を Swift で行ってみました。
Linux(Lubuntu 22.04) の Swift(v5.8) を使用しています。
(Swift のインストールは、UbuntuでSwiftの環境構築を行う方法を参照。
 バージョンは Ubuntu バージョンに揃えます。)

cairo ライブラリで描画した画像を、OpenGLで texture として描画します。
OpenGLglut ライブラリを用い、
Use a C library in Swift on Linux - Stack Overflow
に記述されている方法で利用します。
cairo ライブラリは、
AppKid
GitHub - smumriak/AppKid: UI toolkit for Linux in Swift. Powered by Vulkan
にあるCairoGraphics 関連を参照しました。

実行結果
起動時の画面 (Draw Rects) とメニュー(右クリックで表示)

Draw Circles

Draw Paths

Draw Clip

プログラム
ディレクトリ構成

TestCG
    Package.swift
    Sources
        CCairo
            module.modulemap
        CFreeGLUT
            module.modulemap
        COpenGL
             module.modulemap
        COpenGLU
             module.modulemap
        main.swift
        CGContext.swift

TestCG ディレクトリで、swift package init --type executable を実行する。
CCairo 等の各 module は、ディレクトリを作り modulemap ファイルを作成する。

CCairo
module.modulemap

module CCairo [system] {
    module CairoXlib {
        header "/usr/include/cairo/cairo-xlib.h"
    }
    module Cairo {
        header "/usr/include/cairo/cairo.h"
    }
    link "cairo"
    export *
}

CFreeGLUT
module.modulemap

module CFreeGLUT [system] {
    header "/usr/include/GL/freeglut.h"
    link "glut"
    export *
}

COpenGL
module.modulemap

module COpenGL [system] {
    header "/usr/include/GL/gl.h"
    link "GL"
    export *
}

COpenGLU
module.modulemap

module COpenGLU [system] {
    header "/usr/include/GL/glu.h"
    link "GLU"
    export *
}

Package.swift

// swift-tools-version: 5.8
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
    name: "TestCG",
    targets: [
        // Targets are the basic building blocks of a package, defining a module or a test suite.
        // Targets can depend on other targets in this package and products from dependencies.
        .executableTarget(
            name: "TestCG",
            dependencies: ["COpenGL", "COpenGLU", "CFreeGLUT", "CCairo"],
            path: "Sources"),
        .systemLibrary(
            name: "COpenGL"),
        .systemLibrary(
            name: "COpenGLU"),
        .systemLibrary(
            name: "CFreeGLUT"),
        .systemLibrary(
            name: "CCairo"),
    ]
)

main.swift

// The Swift Programming Language
// https://docs.swift.org/swift-book

//print("Hello, world!")
import COpenGL
import COpenGLU
import CFreeGLUT
import CCairo.Cairo
import Foundation

let width = 640
let height = 480
var drawFlag = 1

func drawRandomPaths(_ context: CGContext, _ width: CGFloat, _ height: CGFloat) -> Void {
   // Draw random paths (some stroked, some filled)
   for i in 0...19 {
      let numberOfSegments = Int.random(in: 0...7)
      let sx = CGFloat.random(in: 0...1) * width
      let sy = CGFloat.random(in: 0...1) * height
      context.move(to: CGPoint(
         x:CGFloat.random(in: 0...1)*width,
         y:CGFloat.random(in: 0...1)*height
      ))

      for j in 0...numberOfSegments {
         if (j % 2 == 0) {
            context.addLine(to: CGPoint(
               x:CGFloat.random(in: 0...1)*width,
               y:CGFloat.random(in: 0...1)*height
            ))
         }
         else {
            context.addCurve(
               to: CGPoint(
                  x:CGFloat.random(in: 0...1)*height,
                  y:CGFloat.random(in: 0...1)*height
               ),
               control1: CGPoint(
                  x:CGFloat.random(in: 0...1)*width,
                  y:CGFloat.random(in: 0...1)*height
               ),
               control2: CGPoint(
                  x:CGFloat.random(in: 0...1)*width,
                  y:CGFloat.random(in: 0...1)*height
               )
            )
         }
      }

      if (i % 2 == 0) {
         context.addCurve(
            to: CGPoint(x:sx, y:sy),
            control1: CGPoint(
               x:CGFloat.random(in: 0...1)*width,
               y:CGFloat.random(in: 0...1)*height
            ),
            control2: CGPoint(
               x:CGFloat.random(in: 0...1)*width,
               y:CGFloat.random(in: 0...1)*height
            )
         )
         context.closePath()
         context.setColor( Double.random(in: 0...1), Double.random(in: 0...1),
            Double.random(in: 0...1),  Double.random(in: 0...1))
         context.fillPath()
            
      }
      else {
            context.lineWidth = CGFloat.random(in: 0...10) + 2
            context.setColor( Double.random(in: 0...1), Double.random(in: 0...1),
                Double.random(in: 0...1),  Double.random(in: 0...1))
            context.strokePath()
      }
   }
}

func draw(_ context: CGContext, _ width: CGFloat, _ height: CGFloat) -> Void {
   // background
   context.setColor(1, 1, 1, 1)
   context.addRect(CGRect(x:0, y:0, width:width, height:height))
   context.fillPath()

   switch (drawFlag) {
      case 1:
      // Draw random rects (some stroked, some filled)
      for i in 0...19 {
         if (i % 2 == 0) {
            context.setColor( Double.random(in: 0...1), Double.random(in: 0...1),
               Double.random(in: 0...1),  Double.random(in: 0...1))
            context.addRect(CGRect(
               x:CGFloat.random(in: 0...1)*width,
               y:CGFloat.random(in: 0...1)*height,
               width:CGFloat.random(in: 0...1)*width,
               height:CGFloat.random(in: 0...1)*height))
            context.fillPath()
         }
         else {
            context.lineWidth = CGFloat.random(in: 0...10) + 2
            context.setColor( Double.random(in: 0...1), Double.random(in: 0...1),
               Double.random(in: 0...1),  Double.random(in: 0...1))
            context.addRect(CGRect(
               x:CGFloat.random(in: 0...1)*width,
               y:CGFloat.random(in: 0...1)*height,
               width:CGFloat.random(in: 0...1)*width,
               height:CGFloat.random(in: 0...1)*height))
            context.strokePath()
         }
      }

      case 2:
      // Draw random circles (some stroked, some filled)
      for i in 0...19 {
         context.addArc(
            center: CGPoint(x:CGFloat.random(in: 0...1)*CGFloat(width),
                            y:CGFloat.random(in: 0...1)*CGFloat(height)),
            radius: CGFloat.random(in: 0...1)*((width>height) ? CGFloat(height) : CGFloat(width)),
            startAngle: 0, endAngle: 2*CGFloat.pi, clockwise: false)
      
         if (i % 2 == 0) {
            context.setColor( Double.random(in: 0...1), Double.random(in: 0...1),
               Double.random(in: 0...1),  Double.random(in: 0...1))
            context.fillPath()
         }
         else {
            context.lineWidth = CGFloat.random(in: 0...10) + 2
            context.setColor( Double.random(in: 0...1), Double.random(in: 0...1),
               Double.random(in: 0...1),  Double.random(in: 0...1))
            context.strokePath()
         }
      }

      case 3:
      // Draw random paths (some stroked, some filled)
      drawRandomPaths(context, width, height)

      case 4:
      // Cliped: Draw random paths (some stroked, some filled)
      context.addArc(
         center: CGPoint(x:width/2, y:height/2),
         radius: (width>height) ? height/2 : width/2,
         startAngle: 0, endAngle: 2*CGFloat.pi, clockwise: false)
      context.closePath()
      context.clip()

      drawRandomPaths(context, width, height)

      context.lineWidth = 1
      context.setColor(0, 0, 0, 1)
      context.strokePath()

      default:
         break
   }
}

// Menu
func showMenuItem(_ val: Int32) {
   switch (val) {
      case 1:
         drawFlag = 1
      case 2:
         drawFlag = 2
      case 3:
         drawFlag = 3
      case 4:
         drawFlag = 4
      case 999:
         glutLeaveMainLoop()
      default:
         break
   }
}

func setupMenus() {
   glutCreateMenu(showMenuItem)

   glutAddMenuEntry("Draw Rects", 1)
   glutAddMenuEntry("Draw Circles", 2)
   glutAddMenuEntry("Draw Paths", 3)
   glutAddMenuEntry("Draw Clip", 4)
   glutAddMenuEntry("Exit", 999)

   glutAttachMenu(GLUT_RIGHT_BUTTON)
}

func display() {
   let cs = cairo_image_surface_create( CAIRO_FORMAT_ARGB32, Int32(width), Int32(height) )
   let c = CGContext(surface: cs, width: width, height: height)


   // draw
   draw(c, CGFloat(width), CGFloat(height))

   var _texture: GLuint = 0
   glGenTextures(1, &_texture);
   glBindTexture(UInt32(GL_TEXTURE_2D), _texture);
   glPixelStorei(UInt32(GL_UNPACK_ALIGNMENT), 1);

   let data = cairo_image_surface_get_data(cs);
   gluBuild2DMipmaps(UInt32(GL_TEXTURE_2D), Int32(GL_RGBA), Int32(width), Int32(height), UInt32(GL_RGBA), UInt32(GL_UNSIGNED_BYTE), data);
   
   glClearColor(0.0, 0.0, 0.0, 0.0)
   glClear(UInt32(GL_COLOR_BUFFER_BIT))
   glOrtho(-1.0, 1.0, -1.0, 1.0, -1.0, 1.0)
   
   glEnable(UInt32(GL_TEXTURE_2D));
   glBindTexture(UInt32(GL_TEXTURE_2D), _texture);
   glColor3f(1,1,1);

   glBegin(UInt32(GL_QUADS));
      glTexCoord2f(0.0, 1.0);
      glVertex2f(-1.0, -1.0);

      glTexCoord2f(0.0, 0.0);
      glVertex2f(-1.0, 1.0);

      glTexCoord2f(1.0, 0.0);
      glVertex2f(1.0, 1.0);

      glTexCoord2f(1.0, 1.0);
      glVertex2f(1.0, -1.0);
   glEnd();

   glFlush()
}

var localArgc = CommandLine.argc
glutInit(&localArgc, CommandLine.unsafeArgv)
glutInitDisplayMode(UInt32(GLUT_SINGLE | GLUT_RGB))
glutInitWindowPosition(80, 80)
glutInitWindowSize(Int32(width), Int32(height))
glutCreateWindow("CairoGraphics")
glutDisplayFunc(display)

setupMenus()
glutMainLoop()

CGContext.swift

// The Swift Programming Language
// https://docs.swift.org/swift-book
//
//  CGContext.swift
//  CairoGraphics
//
//  Created by Serhii Mumriak on 03.02.2020.
//

import Foundation
import CCairo.Cairo

open class CGContext {
    public var surface: Optional<OpaquePointer>
    public var context: Optional<OpaquePointer>
    
    public internal(set) var height: Int = 0
    public internal(set) var width: Int = 0

    public init(surface: Optional<OpaquePointer>, width: Int, height: Int) {
        self.context = cairo_create(surface)
        self.surface = surface
        self.width = width
        self.height = height
    }
}

public extension CGContext {
    func flush() {
        cairo_surface_flush(surface)
    }
}

public extension CGContext {
    func beginPath() {
        cairo_new_path(context)
    }
    
    func closePath() {
        cairo_close_path(context)
    }
    
    var currentPointOfPath: CGPoint {
        var x: Double = .zero
        var y: Double = .zero
        cairo_get_current_point(context, &x, &y)
        return CGPoint(x: x, y: y)
    }
    
    var boundingBoxOfPath: CGRect {
        var x1: Double = .zero
        var y1: Double = .zero
        var x2: Double = .zero
        var y2: Double = .zero
        
        cairo_path_extents(context, &x1, &y1, &x2, &y2)
        
        if x1.isZero && y1.isZero && x2.isZero && y2.isZero {
            return .null
        } else {
            return CGRect(x: min(x1, x2), y: min(y1, y2), width: max(x1, x2) - min(x1, x2), height: max(y1, y2) - min(y1, y2))
        }
    }
}

public extension CGContext {
    func move(to point: CGPoint) {
        cairo_move_to(context, Double(point.x), Double(point.y))
    }
    
    func addLine(to point: CGPoint) {
        cairo_line_to(context, Double(point.x), Double(point.y))
    }
    
    func addRect(_ rect: CGRect) {
        cairo_rectangle(context, Double(rect.origin.x), Double(rect.origin.y), Double(rect.width), Double(rect.height))
    }
    
    func addCurve(to end: CGPoint, control1: CGPoint, control2: CGPoint) {
        cairo_curve_to(context,
                       Double(control1.x), Double(control1.y),
                       Double(control2.x), Double(control2.y),
                       Double(end.x), Double(end.y))
    }
    
    func addQuadCurve(to end: CGPoint, control: CGPoint) {
        let current = currentPointOfPath
        
        let control1 = CGPoint(x: (current.x / 3.0) + (2.0 * control.x / 3.0), y: (current.y / 3.0) + (2.0 * control.y / 3.0))
        let control2 = CGPoint(x: (2.0 * control.x / 3.0) + (end.x / 3.0), y: (2.0 * control.y / 3.0) + (end.y / 3.0))
        
        addCurve(to: end, control1: control1, control2: control2)
    }
    
    func addLines(between points: [CGPoint]) {
        if points.count == 0 { return }
        
        move(to: points[0])
        
        for i in 1..<points.count {
            addLine(to: points[i])
        }
    }
    
    func addRects(_ rects: [CGRect]) {
        for rect in rects {
            addRect(rect)
        }
    }
    
    func addArc(center: CGPoint, radius: CGFloat, startAngle: CGFloat, endAngle: CGFloat, clockwise: Bool) {
        if clockwise {
            cairo_arc_negative(context, Double(center.x), Double(center.y), Double(radius), Double(startAngle), Double(endAngle))
        } else {
            cairo_arc(context, Double(center.x), Double(center.y), Double(radius), Double(startAngle), Double(endAngle))
        }
    }
}

public extension CGContext {
    func fillPath() {
        cairo_fill(context)
    }

    func clip() {
        cairo_clip(context)
    }
    
    func resetClip() {
        cairo_reset_clip(context)
    }
    
    func strokePath() {
        cairo_stroke(context)
    }
}

public extension CGContext {
    func fill(_ rect: CGRect) {
        beginPath()
        addRect(rect)
        closePath()
    }
    
    func stroke(_ rect: CGRect) {
        beginPath()
        addRect(rect)
        closePath()
        strokePath()
    }
}

public extension CGContext {
    func setColor(_ r:Double, _ g:Double, _ b:Double, _ a:Double) {
        cairo_set_source_rgba(context, r, g, b, a);
    }
    
    var lineWidth: CGFloat {
        get {
            return CGFloat(cairo_get_line_width(context))
        }
        set {
            cairo_set_line_width(context, Double(newValue))
        }
    }    
}