目次

はじめに
使用環境
第1章 画面の表示とSkiaによる描画
1.1 Skiaとは?
1.2 LWJGL+GLFW
1.3 プロジェクトの用意
1.4 画面を作る
第2章 Skiaで遊ぼう
2.1 図形の描画
2.2 文字の描画
2.3 移動・変形
2.4 状態の保存と復元
2.5 透明度の適用
2.6 切り取り
第3章 TaskRunnerの実装
3.1 マルチスレッドについて
3.2 Flutterの内部構造
3.3 実装計画
3.4 Task Runnerを実装する
3.5 GLViewを実装する
3.6 Rasterizerを実装する
3.7 Shellを実装する
3.8 mainからEngine関連のコードを呼ぶ
第4章 Layerツリーの実装と表示
4.1 Layerツリーとは?
4.2 Layerツリー描画の詳細
4.3 レイアウトに関する寸法の表現
4.4 実装計画
4.5 寸法用のクラスを実装する
4.6 レイヤを実装する
4.7 Layerツリーを表示する
4.8 さまざまな効果をもつレイヤを追加
4.9 発展
第5章 RenderツリーからLayerツリーの構築
5.1 描画パイプライン
5.2 Renderツリーとは?
5.3 RenderツリーからLayerツリーの生成手法
5.4 ボックスレイアウトモデルと制約
5.5 実装計画
5.6 PaintingContextを実装する
5.7 BoxConstraintsを実装する
5.8 RenderObjectを実装する
5.9 RenderPipelineを実装する
5.10 Renderツリーから画面を描画する
第6章 色々なRenderObject
6.1 RenderPositionedBox
6.2 RenderFlex
6.3 RenderCustomClip
第7章 Elementツリー・Widgetツリーの構築1
7.1 3つのツリー
7.2 各ツリーの根
7.3 Elementツリー生成の流れ
7.4 Elementツリーの登場人物
7.5 実装計画
7.6 RenderObjectを修正する
7.7 Elementを実装する
7.8 利用するWidgetを実装する
第8章 Elementツリー・Widgetツリーの構築2
8.1 実装計画
8.2 複数の子をもつRenderObjectの修正
8.3 MultiChildRenderObjectElementを実装する
8.4 利用するWidgetを実装する
8.5 信号機をWidgetツリーで構築する
第9章 テキストの表示
9.1 RichTextについて
9.2 実装計画
9.3 RenderParagraphを実装する
9.4 RichTextの実装
9.5 テキストを表示してみる
第10章 runAppとWidgetsFlutterBinding
10.1 runApp()
10.2 WidgetsFlutterBindingとは?
10.3 実装計画
10.4 runApp()とEngineの準備
10.5 Shellの修正
10.6 WidgetsFlumeBindingの実装
10.7 main関数でrunApp()を呼ぶ
第11章 Engineの隠蔽と画面の更新タイミング
11.1 Engineの隠蔽方法
11.2 scheduleFrameとvsyncによる画面更新
11.3 実装計画
11.4 Layerツリーをディープコピーできるようにする
11.5 TaskRunnerの修正
11.6 インターフェスの定義
11.7 WidgetsFlumeBindingの修正
11.8 Shellの修正
11.9 runFlume()の実装
11.10 main関数の修正
第12章 Engineのキー入力対応
12.1 キーイベントとは
12.2 EngineからFrameworkへのキーイベントの受け渡し
12.3 実装計画
12.4 KeyEventの実装
12.5 KeyboardControllerの実装
12.6 GLViewの修正
12.7 WidgetsFlumeBindingの修正
12.8 Shellの修正
12.9 キーボードを使って信号機を点滅させる
第13章 markNeedsPaint()
13.1 flushPaintで行われていること
13.2 実装計画
13.3 RenderObjectの更新
13.4 PaintingContextの実装
13.5 RenderPipelineの実装
13.6 子をもつRenderObjectのinterfaceの更新
13.7 Elementの更新
13.8 各RenderObjectの修正
13.9 WidgetsFlumeBindingの実装
13.10 画面の表示
第14章 markNeedsLayout()
14.1 ノードのdepth
14.2 RelayoutBoundary
14.3 実装計画
14.4 BoxConstraintsの更新
14.5 RenderObjectの更新
14.6 RenderPipelineの更新
14.7 RenderObjectのinterfaceの更新
14.8 各RenderObjectの更新
14.9 プログレスバーのようなものの作成
第15章 WidgetにGenericsを適用しよう
15.1 Dartのオーバーライドの柔軟性
15.2 Genericsによる代替
15.3 実装計画
15.4 RenderObjectWidgetの更新
15.5 各Widgetの更新
15.6 Elementの更新
第16章 Widgetツリーの差分計算1
16.1 Elementツリーのリビルド
16.2 根ノードの更新の流れ
16.3 ひとつの子をもつElementの更新の流れ
16.4 RenderObjectの一生
16.5 実装計画
16.6 RenderObjectの更新
16.7 各RenderObjectのプロパティーの修正
16.8 Widgetの更新
16.9 BuildOwnerの実装
16.10 Elementの更新
16.11 RenderObjectElementの更新
16.12 RenderObjectToWidgetElementの更新
16.13 SingleChildRenderObjectElementの更新
16.14 WidgetsFlumeBindingの修正
16.15 動かしてみる
第17章 Widgetツリーの差分計算2
17.1 複数の子の差分計算
17.2 実装計画
17.3 各RenderObjectの更新
17.4 Widgetの更新
17.5 updateChildren()の実装
17.6 MultiChildRenderObjectElementの更新
17.7 信号機を動かしてみる
第18章 StatefulWidgetとStatelessWidgetの実装
18.1 ComponentElementの生成と更新
18.2 StatelessElementとStatefulElement
18.3 実装計画
18.4 BuildContextの実装
18.5 StatelessWidgetの実装
18.6 StatefulWidgetの実装
18.7 ComponentElementの実装
18.8 StatelessElementの実装
18.9 StatefulElementの実装
18.10 信号機をStatefulWidgetで書き直してみよう
第19章 ○×ゲームを作ってみよう
第20章 ポインターイベントの処理
20.1 PointerEventとは
20.2 Widgetへの分配
20.3 実装計画
20.4 Engineの修正
20.5 HitTest周りのクラスを追加
20.6 RenderObjectの修正
20.7 Listenerの実装
20.8 WidgetsFlumeBindingの修正
20.9 ポインタの動きを追うアプリを作る
第21章 アニメーションの実装
21.1 Flutterのアニメーション利用
21.2 アニメーションシステムの内部構造
21.3 実装計画
21.4 TimingMeasurerとFrameworkへのタイムスタンプの受け渡し
21.5 transientCallbacksの実装
21.6 CurveとSimulationの実装
21.7 TickerとTickerProviderの実装
21.8 AnimationControllerの実装
21.9 FadeTransitionの実装
21.10 押すとアニメーションする箱を作る
第22章 InheritedWidgetの実装
22.1 InheritedWidgetとは
22.2 _updateInheritance()とInheritedElement
22.3 InheritedWidgetにアクセスする
22.4 実装計画
22.5 BuildContextの修正
22.6 Elementの修正
22.7 InheritedElementの実装
22.8 InheritedWidgetの実装
22.9 InheritedWidgetを用いたアプリケーションの作成
第23章 ○×ゲームを進化させよう

はじめに

"What I cannot create, I do not understand. Know how to solve every problem that has been solved." -- Richard Feynman

 Widgetを組み合わせるだけでさまざまな画面を作ることができるFlutterですが、なぜそのようなことが可能なのでしょうか?本書ではFlutterを自身で1から実装し直し、Flumeという新たなUIフレームワークを作ります。再実装を通じて、UIフレームワークを支える複雑かつ壮麗な世界の理解を目指します。

 Flutter自体はすでに巨大かつ複雑なソフトウェアであり、隅々まで実装するのは困難です。そのため、Flumeでは本質的に重要な機能のみをピックアップして説明・実装することにします。機能を絞るため、もちろん実用に足るフレームワークを作ることは叶いません。しかし、UIの管理手法というのはFlutterだけでなく、Webフレームワークやブラウザー、AndroidやiOSなどのネイティブアプリでも共通して利用されています。まずはシンプルなFlumeの仕様を理解することで、本家Flutterや他のフレームワークのコードを理解する後押しになればよいと考えています。

使用環境

 ・Kotlin/JVM

 ・Kotlin Gradle

 ・LWJGL+OpenGL+GLFW

 ・Skiko

 Flumeを実装するに当たり、例として紹介するのは、すべてKotlin/JVMでの実装になります。しかし描画側のライブラリーは比較的多くの言語や環境で使えるものであるため、好きな言語・バックエンドで書くことも可能だと考えています(もし別言語で実装した場合は共有してくれると嬉しいです)。

 1巻では、FlutterがWidgetをどのように内部で処理し、画面に表示しているのかというところを追い、自身で再実装します。

第1章 画面の表示とSkiaによる描画

 UIフレームワークの旅は、画面を表示するところから始まります。

 0から画面を作るというとき、本来ならばOpenGLなどの低レイヤなグラフィックスライブラリーを使うべきなのかもしれません。が、そこから始めてしまうとCGの学習が大きくのしかかってくるため、少しだけ横着して、Skiaというライブラリーから出発することにします。

 本章の実装のコード・差分はこちらに掲載しています。

 https://github.com/organic-nailer/flume/commit/50065b

1.1 Skiaとは?

 Skia1は2Dのグラフィックスライブラリーで、Googleを中心としてオープンソースで開発が続けられています。ChromiumやAndroid、Flutterなど多くの巨大プロダクトで採用されているため、利用側としては安定して使える安心なライブラリーです。環境を問わず同じグラフィックを提供できるということで、FlutterやXamarin.Forms、Compose for Desktop等、クロスプラットフォームのバックエンドとしての採用も目立ちます。

 Skiaを使う一番の理由は、Flutterで使われているためです。簡単な描画はFlutterのCustomPaintなどで使うものとほとんど同じであり、Flutter使いとしては扱いやすいものになっています。

 また、Skiaは各言語のバインディングが豊富なのも特徴です。観測した中でも

 ・Skia: C++

 ・Skija: Java

 ・Skiko: Kotlin

 ・skia-python: Python

 ・SkiaSharp: C#/.NET

 ・rust-skia: Rust

 ・CanvasKit: WASM

 ・node-skia: Node.js

 ・SkiaKit: Swift

 といったものがあります。今回はKotlinで書くので、Skiko2を利用します。SkikoはJetbrainsの管理するプロジェクトで、Compose for Desktopなどにも採用されています。安心ですね!

1.2 LWJGL+GLFW

 Skiaはバックエンドとして、OpenGLやVulkanなど複数のライブラリーが選択できます。今回はJVMでのOpenGLとウィンドウ表示の提供元として、LWJGL3というライブラリーを利用します。

 ゲーム用のライブラリーなので、多少大きいです。OpenGL(GLFW)さえ使えれば、何のライブラリーでもあまり変わらない実装ができると思います。

1.3 プロジェクトの用意

 さて、早速実装を始めましょう。Intellij IDEAでKotlinのプロジェクトを作成します。Intellij IDEA Community版4であれば、無料で使うことができます。

 起動画面の[New Project]から新規プロジェクトの画面に行き、

 ・New Project

 ・Language: Kotlin

 ・Build system: Gradle

 ・Gradle DSL: Kotlin

 ・Add sample code

 を選択します。プロジェクト名はflume-なんとかにしておきましょう。

図1.1:

 [Create]を押すと、プロジェクトが作られます。しばらくすると、src/main/kotlin/Main.ktのmain関数の左に緑矢印が出てくるので、そこから実行できます。

図1.2: Hello, world!

 Hello Worldが出力されるのを確認しましょう。これでひとまず、プロジェクトの用意は完了です。

1.4 画面を作る

 続いて、ウィンドウが表示できるようにしましょう。まず、LWJGLとSkikoを導入します。

1.4.1 build.gradleの記述

 プロジェクトのルートにあるbuild.gradle.ktsへ次の記述をしましょう。

リスト1.1: build.gradle.kts

plugins {
    kotlin("jvm") version "1.6.10"
}

group = "dev.fastriver"
version = "1.0-SNAPSHOT"
val skArtifact = "skiko-awt-runtime-windows-x64" // x64 Windowsの場合
// val skArtifact = "skiko-awt-runtime-macos-x64" // intel Macの場合
// val skArtifact = "skiko-awt-runtime-linux-arm64" // arm Linuxの場合
val skVersion = "0.7.18"
val lwjglVersion = "3.3.1"
val lwjglNatives = "natives-windows" // x64 Windowsの場合
// val lwjglNatives = "natives-macos" // intel Macの場合
// val lwjglNatives = "natives-linux-arm64" // arm Linuxの場合

repositories {
    mavenCentral()
    maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
}

dependencies {
    testImplementation(kotlin("test"))
    api("org.jetbrains.skiko:${skArtifact}:${skVersion}")
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.1")

    implementation("org.lwjgl:lwjgl:$lwjglVersion")
    implementation("org.lwjgl:lwjgl-glfw:$lwjglVersion")
    implementation("org.lwjgl:lwjgl-opengl:$lwjglVersion")
    runtimeOnly("org.lwjgl:lwjgl:$lwjglVersion:$lwjglNatives")
    runtimeOnly("org.lwjgl:lwjgl-glfw:$lwjglVersion:$lwjglNatives")
    runtimeOnly("org.lwjgl:lwjgl-opengl:$lwjglVersion:$lwjglNatives")
}

 Skikoのバージョンと環境は次のリンクを確認して、最新のものを利用してください5

 https://github.com/JetBrains/skiko/releases

 LWJGLのバージョンは次のリンクを確認してください。

 https://mvnrepository.com/artifact/org.lwjgl/lwjgl

1.4.2 画面表示の実装

 続いて、Main.ktを書き換えます。

リスト1.2: Main.kt

import org.jetbrains.skia.*
import org.lwjgl.glfw.GLFW.*
import org.lwjgl.opengl.GL
import org.lwjgl.opengl.GL11

fun main() {
    val width = 640
    val height = 480

    glfwInit()
    glfwWindowHint(GLFW_VISIBLE, GLFW_FALSE)
    glfwWindowHint(GLFW_RESIZABLE, GLFW_TRUE)
    val windowHandle = glfwCreateWindow(width, height, "Flume", 0, 0)
    glfwMakeContextCurrent(windowHandle)
    glfwSwapInterval(1)
    glfwShowWindow(windowHandle)


    GL.createCapabilities()

    val context = DirectContext.makeGL()

    val fbId = GL11.glGetInteger(0x8CA6)
    val renderTarget =
        BackendRenderTarget.makeGL(width, height, 0, 8, fbId, FramebufferFormat.GR_GL_RGBA8)

    val surface = Surface.makeFromBackendRenderTarget(context,
        renderTarget,
        SurfaceOrigin.BOTTOM_LEFT,
        SurfaceColorFormat.RGBA_8888,
        ColorSpace.sRGB)!!

    val canvas = surface.canvas

    while (!glfwWindowShouldClose(windowHandle)) {

        val paintGreen = Paint().apply { color = 0xFF8BC34A.toInt() }
        canvas.drawCircle(100f, 100f, 40f, paintGreen)

        val paintYellow = Paint().apply { color = 0xFFFFEB3B.toInt() }
        canvas.drawCircle(200f, 100f, 40f, paintYellow)

        val paintRed = Paint().apply { color = 0xFFF44336.toInt() }
        canvas.drawCircle(300f, 100f, 40f, paintRed)

        context.flush()
        glfwSwapBuffers(windowHandle)
        glfwPollEvents()
    }

}

 説明はあとに回すとして、これを実行すると、信号機のようなものがウィンドウ内に描かれた状態で開くことが確認できます。

図1.3:

 これができれば第一関門は突破です!後はこのウィンドウに落書きする仕組みを作るだけです。

1.4.3 なにが起こっているのか

 先へ進む前に少しコードを読みましょう。

 main関数は、プログラムの実行開始時に呼ばれる最初の地点です。最初の十数行はGLFWでウィンドウを作るための設定を書いています。個々の関数の詳細はここでは重要ではないので、このように書けば画面サイズを指定してウィンドウを表示できる、と覚えておきましょう。

リスト1.3:

val width = 640
val height = 480

glfwInit()
glfwWindowHint(GLFW_VISIBLE, GLFW_FALSE)
glfwWindowHint(GLFW_RESIZABLE, GLFW_TRUE)
val windowHandle = glfwCreateWindow(width, height, "Flume", 0, 0)
glfwMakeContextCurrent(windowHandle)
glfwSwapInterval(1)
glfwShowWindow(windowHandle)


GL.createCapabilities()

val context = DirectContext.makeGL()

val fbId = GL11.glGetInteger(0x8CA6)
val renderTarget =
    BackendRenderTarget.makeGL(width, height, 0, 8, fbId, FramebufferFormat.GR_GL_RGBA8)

 続いては、Skiaの設定を行う部分になります。Surface6というものを用意しています。

 絵を描いたときにそれをピクセルとして管理してくれているようです。私達がこのSurfaceに絵を描く際には、下のCanvas7を利用します。Canvasは画面に描画するための簡単なインターフェスを提供してくれるクラスです。

リスト1.4:

val surface = Surface.makeFromBackendRenderTarget(context,
    renderTarget,
    SurfaceOrigin.BOTTOM_LEFT,
    SurfaceColorFormat.RGBA_8888,
    ColorSpace.sRGB)!!

val canvas = surface.canvas

 設定の記述の後、メインループに入ります。glfwWindowShouldCloseでウィンドウが閉じたかどうかを判定しており、閉じない限りループを続けます。ループ内ではcanvasに描画し、SwapBufferすることで画面に表示されます。ここでは3色で3箇所に円を描く命令を記述しているため、信号機の模様が描かれます。

リスト1.5:

while (!glfwWindowShouldClose(windowHandle)) {

    val paintGreen = Paint().apply { color = 0xFF8BC34A.toInt() }
    canvas.drawCircle(100f, 100f, 40f, paintGreen)

    val paintYellow = Paint().apply { color = 0xFFFFEB3B.toInt() }
    canvas.drawCircle(200f, 100f, 40f, paintYellow)

    val paintRed = Paint().apply { color = 0xFFF44336.toInt() }
    canvas.drawCircle(300f, 100f, 40f, paintRed)

    context.flush()
    glfwSwapBuffers(windowHandle)
    glfwPollEvents()
}

 このようにして、ふたつのライブラリーだけを用いて画面の表示、そして画面に好きな模様を表示させることができました。

第2章 Skiaで遊ぼう

 前章ではSkiaで円を描画しましたが、Skiaには他にもさまざまな機能があります。Flumeの実装で使うものを中心に紹介します。

2.1 図形の描画

 円だけでなく、長方形、線など、いろいろと描くことができます。

リスト2.1:

while (!glfwWindowShouldClose(windowHandle)) {

    val paintGreen = Paint().apply { color = 0xFF8BC34A.toInt() }
    canvas.drawCircle(100f, 100f, 40f, paintGreen)

    val paintYellow = Paint().apply { color = 0xFFFFEB3B.toInt() }
    canvas.drawRect(Rect.makeXYWH(200f, 100f, 50f, 100f), paintYellow)

    val paintRed = Paint().apply { color = 0xFFF44336.toInt() }
    canvas.drawLine(300f, 100f, 400f, 200f, paintRed)

    context.flush()
    glfwSwapBuffers(windowHandle)
    glfwPollEvents()
}
図2.1:

2.2 文字の描画

 文字は少しややこしいですが、描画できます。

 ・ParagraphStyle: 文字のスタイルを指定

 ・FontCollection: 文字のフォントを指定

 ・ParagraphBuilder: Paragraphを用意するためのクラス

 ・Paragraph: 文字の描画情報をもつクラス

リスト2.2:

val style = ParagraphStyle().apply {
    textStyle = TextStyle().apply {
        color = 0xFFFF0000.toInt()
        fontSize = 30f
    }
}
val font = FontCollection().apply {
    setDefaultFontManager(FontMgr.default)
}
val builder = ParagraphBuilder(
    style, font
)
builder.addText("Flumeフルーム古め, Hello Flume!!")
val paragraph = builder.build()

while (!glfwWindowShouldClose(windowHandle)) {

    paragraph.layout(300f)
    paragraph.paint(canvas, 100f, 100f)

    context.flush()
    glfwSwapBuffers(windowHandle)
    glfwPollEvents()
}

 まず、ParagraphStyleとFontCollectionを用意して、ParagraphBuilderに渡します。ParagraphBuilder.addText()で表示したい文字を設定できます。その後、ParagraphBuilder.build()でParagraphオブジェクトを生成します。ここまでは描画の前に済ませておくことが可能です。実際に描画の段になったら、Paragraph.layout()で最大幅を指定して、大きさを計算します1。もし一行で収まらない場合は、標準で改行されます。

 最後にcanvasを使ってParagraph.paint()に左上の座標を指定し、描画します。すると次のように、きれいに文字を描いてくれます。

図2.2:

2.3 移動・変形

 canvas.transform()を使うと、描画位置をずらすことができます。

リスト2.3:

  while (!glfwWindowShouldClose(windowHandle)) {

      val paintRed = Paint().apply { color = 0xFFFF0000.toInt() }
      val paintGreen = Paint().apply { color = 0xFF00FF00.toInt() }

      canvas.drawRect(Rect.makeXYWH(100f,100f,100f,100f), paintRed)

      canvas.translate(200f, 0f)

      canvas.drawRect(Rect.makeXYWH(100f,100f,100f,100f), paintGreen)

      canvas.translate(-200f, 0f)

      context.flush()
      glfwSwapBuffers(windowHandle)
      glfwPollEvents()
  }

 赤の四角と緑の四角は同じ場所を指定していますが、緑を描く前にcanvasに対して移動を適用しているため、右に200だけずれた位置に四角が描かれます。

図2.3:

 直接射影変換行列を適用することもできます。以下では30°回転させました。

リスト2.4:

while (!glfwWindowShouldClose(windowHandle)) {

    val paintRed = Paint().apply { color = 0xFFFF0000.toInt() }
    val paintGreen = Paint().apply { color = 0xFF00FF00.toInt() }

    canvas.drawRect(Rect.makeXYWH(100f,100f,100f,100f), paintRed)

    canvas.translate(200f, 0f)

    val rot = (30 * PI / 180).toFloat()
    canvas.concat(Matrix33(
        cos(rot), -sin(rot), 0f,
        sin(rot), cos(rot), 0f,
        0f,0f,1f
    ))

    canvas.drawRect(Rect.makeXYWH(100f,100f,100f,100f), paintGreen)

    canvas.concat(Matrix33(
        cos(-rot), -sin(-rot), 0f,
        sin(-rot), cos(-rot), 0f,
        0f,0f,1f
    ))

    canvas.translate(-200f, 0f)

    context.flush()
    glfwSwapBuffers(windowHandle)
    glfwPollEvents()
}
図2.4:

 ちなみに、内部的にはただの2次元の射影変換2なので、順に移動→回転した結果が得られています3

2.4 状態の保存と復元

 移動や変形などを適用した後は、もとの状態に戻さないと面倒なことになります。前の例では後で逆行列を掛けて元に戻していましたが、canvas.save()とcanvas.restore()を使うと、もっと単純に記述可能です。

リスト2.5:

while (!glfwWindowShouldClose(windowHandle)) {

    val paintRed = Paint().apply { color = 0xFFFF0000.toInt() }
    val paintGreen = Paint().apply { color = 0xFF00FF00.toInt() }

    canvas.drawRect(Rect.makeXYWH(100f,100f,100f,100f), paintRed)

    canvas.save() //現時点でのmatrixを保存

    canvas.translate(200f, 0f)

    val rot = (30 * PI / 180).toFloat()
    canvas.concat(Matrix33(
        cos(rot), -sin(rot), 0f,
        sin(rot), cos(rot), 0f,
        0f,0f,1f
    ))

    canvas.drawRect(Rect.makeXYWH(100f,100f,100f,100f), paintGreen)

    canvas.restore() // matrixを前に保存した状態に戻す

    context.flush()
    glfwSwapBuffers(windowHandle)
    glfwPollEvents()
}

 変換前にcanvas.save()を呼び出しておけば、canvas.restore()で変換をなかったことにできます。描画内容は削除されません。また、Stackのような挙動であるため、入れ子でsave/restoreしても直近の内容のみが復元されます。

2.5 透明度の適用

 一部の描画を半透明にしたい場合、canvas.saveLayer()を透明度を指定したPaintとセットで呼ぶことにより、後ろを透明にできます。これは変形のsave()と同様にrestore()を呼ぶことで、透明度の指定をやめられます。

リスト2.6:

while (!glfwWindowShouldClose(windowHandle)) {
    canvas.clear(0xFFFFFFFF.toInt())

    val paintRed = Paint().apply { color = 0xFFFF0000.toInt() }
    val paintGreen = Paint().apply { color = 0xFF00FF00.toInt() }

    canvas.drawRect(Rect.makeXYWH(100f,100f,100f,100f), paintRed)

    val paintOpacity = Paint().apply { alpha = 50 }
    canvas.saveLayer(Rect.makeXYWH(0f,0f, width.toFloat(), height.toFloat()), paintOpacity)

    canvas.drawRect(Rect.makeXYWH(300f,100f,100f,100f), paintGreen)

    canvas.restore()

    context.flush()
    glfwSwapBuffers(windowHandle)
    glfwPollEvents()
}

 重ねて書かれると透明度が機能しないのでループの最初にキャンバスをリセットする行を追加しました

図2.5:

2.6 切り取り

 Pathを使うと、自由な形で表示を切り取ることができます。切り取りたい空間を囲むPathを定義し、canvas.clipPath()を呼ぶと、その箇所だけ切り取って表示されます。これもrestore()すると削除されます。

リスト2.7:

while (!glfwWindowShouldClose(windowHandle)) {

    canvas.clear(0xFFFFFFFF.toInt())

    val paintRed = Paint().apply { color = 0xFFFF0000.toInt() }
    val paintGreen = Paint().apply { color = 0xFF00FF00.toInt() }

    canvas.drawRect(Rect.makeXYWH(100f,100f,100f,100f), paintRed)

    canvas.save()

    canvas.translate(300f, 100f)

    val path = Path().apply {
        lineTo(0f, 100f - 30f)

        quadTo(100f / 4, 100f, 100f / 2, 100f)

        quadTo(100f - 100f / 4, 100f, 100f, 100f - 30f)

        lineTo(100f, 0f)
        closePath()
    }

    canvas.clipPath(path, antiAlias = true)

    canvas.drawRect(Rect.makeXYWH(0f,0f,100f,100f), paintGreen)

    canvas.restore()

    context.flush()
    glfwSwapBuffers(windowHandle)
    glfwPollEvents()
}
図2.6:

 Skiaはドキュメントが絶望的に少ないので、API Referenceをじっくり読むか、誰かの書いた実装を読むなりしましょう。当然Flutter Engine内でも使われている4ので、そこから始めるのもありです。

試し読みはここまでです。
この続きは、製品版でお楽しみください。