Skia(Skija)でParagraphを描画する

Skiaでテキストを描画するのにParagraphを利用したい

# なぜParagraph?

A. FlutterのText描画に使われているから

# 環境

- Kotlin/JVM
- lwjgl(OpenGLのラッパー)
- Skija(SkiaのJavaラッパー)

```kotlin:build.gradle.kts
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

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

group = "dev.fastriver"
version = "1.0-SNAPSHOT"
val skArtifact = "skija-windows"
val skVersion = "0.92.20"
val lwjglVersion = "3.2.3"
val lwjglNatives = "natives-windows"

repositories {
    mavenCentral()
    maven (url = "https://packages.jetbrains.team/maven/p/skija/maven")
}

dependencies {
    testImplementation(kotlin("test"))
    api("org.jetbrains.skija:${skArtifact}:${skVersion}")

    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")
}

tasks.test {
    useJUnit()
}

tasks.withType<KotlinCompile>() {
    kotlinOptions.jvmTarget = "1.8"
}
```

# 手順

## 画面表示の下準備

OpenGLの初期化とSkiaのSurfaceを作るまで

```kotlin:main.kt
fun main() {
    val width = 640
    val height = 480

    glfwInit()
    glfwWindowHint(GLFW_VISIBLE, GLFW_FALSE)
    glfwWindowHint(GLFW_RESIZABLE, GLFW_TRUE)
    val windowHandle = glfwCreateWindow(width,height,"Skija Sample", 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.getSRGB()
    )

    val canvas = surface.canvas

    while(!glfwWindowShouldClose(windowHandle)) {
        /*
        ここで描画処理 
        */
        context.flush()
        glfwSwapBuffers(windowHandle)
        glfwPollEvents()
    }
}
```

## ParagraphBuilderの作成

ParagraphBuilderの作成にはParagraphStyleとFontCollectionが必要なので同時に作る。FontCollectionはデフォルトに設定(未設定だと描画できない)。ParagraphStyleのプロパティはFlutterのTextの設定に似ている。

これらはcanvasと無関係なのでループ外の記述でOK。

```kotlin
val style = ParagraphStyle().apply {
    alignment = Alignment.START
    direction = Direction.LTR
    textStyle = textStyle.apply {
        fontSize = 50.0f
        maxLinesCount = 2L
        color = 0xFFFF0000.toInt()
    }
}
val font = FontCollection().apply {
    setDefaultFontManager(FontMgr.getDefault())
}
val builder = ParagraphBuilder(style, font)
```

## Paragraphのビルドとレイアウト

- addText: 表示する文字の指定
- ParagraphBuilder.build: Paragraphを作成
- Paragraph.layout: 幅を指定してレイアウト
    - これを忘れるとまた描画されない

```kotlin
builder.addText("Draw Paragraph")
val paragraph = builder.build()
paragraph.layout(800f)
```

## 表示

Paragraph.paintでcanvasとOffsetを指定して描画できる。

```kotlin
while(!glfwWindowShouldClose(windowHandle)) {

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

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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/177771/708ddc31-b8be-84e9-abf8-0052c9f44eed.png)


# 参考

https://skia.org/docs/user/modules/quickstart/#text-shaping

https://stackoverflow.com/questions/59261257/skparagraph-not-shaping-or-rendering-text