Typstで☃のマフラーの色を指定する方法

[Typst]の重要な目的の一つは「LaTeXのよりよい代替となること」です。そして、皆さんご存知の通り[^known]、LaTeXといえば<strong>[ゆきだるま][snowman-1]</strong>:snowman:です。となると、Typstでも:snowman:ができることが求められるのは当然の帰結でしょう[^satysfiadvent]。

[^known]: えっ、ご存知でしょ？:snowman:
[^satysfiadvent]: アッ、[某“Typst以外”の記事][satysfiadvent]の書き出しの使い回しだ！:astonished:

というわけで、本記事では**​「好きな色のマフラーを付けたゆきだるま:snowman:を出力する」​**ことに関して、とりとめなく話したいと思います:bow:

## TeXのConfの話の話

いきなり“Typst以外”の話で恐縮ですが、先月末（2024年11月30日）に富山において[「TeXConf 2024」][texconf2024][^texconf]が開催されました。その最後を飾ったのが鹿野桂一郎氏による「文字の一部に色を塗る」と題した講演です。

[^texconf]: TeXConfはTeX・組版・出版とその周辺に関する知見を共有することを目的とした会議です。

* [文字の一部に色を塗る][shikano-conf]（GitHub／k16shikano）

この講演では「単一の文字を**複数の色**に塗り分けて出力する」というタスクについて種々の文書作成ソフトウェア上での実現方法について述べています。具体的には2種類の実装方針を紹介しています。

|![image-1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14550/f2028537-3c8d-b61d-489d-6daa0f4af38c.png)|
|:-:|
|グリフパスのクリップを利用する方法<br>（PostScriptでの実装）|

|![image-2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14550/cff1eff9-bff4-79bf-ec4a-0ed7a5730a2d.png)|
|:-:|
|TikZ上で実現する方法<br>（LaTeX＋TikZでの実装）|

:::note info
腕に覚えのあるTypstユーザの皆さんは、Typst上で「文字の一部に色を塗る」ことに挑戦してみるのもいいでしょう:information_desk_person:
:::

それに続いて「ゆきだるまの字形☃の**マフラーに色を付ける**」という発展的な課題について話していたようです[^timeup]。

[^timeup]: あるいは、時間切れのため話していないのかもしれません。（TeXConf 2024に参加して**いない**人並感 :upside_down:）

|![image-3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14550/1e3d484b-8e1b-be17-0f52-767ce463c740.png)|
|:-:|
|:snowman:のマフラーに色を塗る|

:::note info
具体的には「任意に与えられた☃の字形について、前処理を許容した上で、マフラー部分の領域を自動的に取得して別の色を塗る」という問題を扱っています。前処理を行えるのでグリフパスの取得が可能ですが、☃の字形中でマフラーの部分が他から独立しているわけでなく、そもそもどの部分がマフラーであるのか事前には判らないため、話は簡単ではありません。講演では機械学習を利用したアプローチを紹介しています。
:::

鹿野氏の講演はとても面白い話なのですが、でも本記事の話題はこの講演の話とは直接は関係ありません。ただ「ゆきだるま:snowman:の**マフラーに色を付ける**」というところだけが関係しています。

## :snowman:のConfの話の話

Typstの動向を早くから注視していた人であれば、「:snowman:のマフラーに色を付ける」と聞いてsc1Typstのことを思い出したかもしれません。

昨年（2023年）の11月11日（1が4つ並ぶ**きりたんぽの日**です:smiley:）に某ツイッタァー（現𝕏）において[「ﾅﾝﾄｶConf 2023」][nantokaconf2023][^nantokaconf]が開催されました。その最後[^count]を飾ったのが某ZR氏による「アレをもっともっと画期的にする話」と題した講演です。

* [アレをもっともっと画期的にする話][zr-conf]（GitHub／zr-tex8r）

その講演において「:snowman:」と「1」にちなんで紹介されたのが「1を入力して:snowman:のマフラーに色を付ける」ための画期的なTypstモジュールである**sc1Typst**です。

[^nantokaconf]: ﾅﾝﾄｶConfは:snowman:・:snowman2:とその周辺に関する知見を共有することを目的とした会議です。
[^count]: なお「ﾅﾝﾄｶConf 2023」の講演数は全部で1件でした。

* [sc1Typst ― もっともっと画期的なTypst用文書テンプレート][gist-sc1typst]（Gist／zr-tex8r）

sc1Typstを利用したTypst文書では多数の「1」を入力すること文書ソースを作成します。

```text
#import "sc1typst.typ":*; #show:_1
1
1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111
11111111111111111111111111111111111
```

この文書ファイルを通常の手順でTypstでコンパイルすると、マフラーに色がついた:snowman:が出力されます。実は、このマフラーの色は先ほどのソース中に書いた「1」の数から決まるようになっています。

|![image-4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14550/b3556bbf-21fe-0f1f-576c-b758365f3d8d.png)|
|:-:|
|出力結果|

ところで、この出力中の:snowman:は（鹿野氏のスライドのものと同じ外見であることから判るように）**フォントの字形**です。つまり、「ゆきだるまの字形☃のマフラーに色を付ける」ネタに関しては、Typstの方がLaTeXよりも1年も先行していたことになります。やっぱり時代はTypstですね:wink:<sub>（えっ）</sub>

:::note info
ただし、sc1Typstが使うフォントは固定（[原ノ味明朝][haranoaji][^haranoaji]）なので「予め抽出したグリフパスのデータを実装コードに直書きする」ことができます。なのでこの辺りについて技術的に面白い点はありません:upside_down:
:::

[^haranoaji]: [Typst App][typst-app]で使用可能な日本語用フォントの一つです。

時代の話は措いておきましょう。世の中には「ゆきだるまの字形☃のマフラーに色を付ける」機能はぜひとも欲しいが一方で「『1』を並べて入力する」方式は全く不要という人もいるでしょう。そういう人にとっては「1」を並べる方式のsc1Typstは向いていないでしょう。

とはいってもsc1Typstの実装コードの中には「マフラーに色を付ける」処理が書かれているはずで、そこだけを抽出して新しいモジュールを作ればよさそうです。何といっても、Typstのスクリプト言語はTeX言語:scream:と違ってとても**読みやすい**ことで有名です。早速sc1Typstの実装コードを眺めてみましょう:smiley:

|![image-5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14550/9ebbd664-9208-d930-52e3-05ccd9c5b2eb.png)|
|:-:|
|（某ZR氏のスライドより抜粋）|

11月11日のネタ用のコードであるためか、**チョット読みにくい**:christmas_tree:ですね……（ざんねん:upside_down:）

（以上で長い長い長い前フリ終わり:upside_down:）

## フツーのTypstモジュールの話

というわけで、「ゆきだるまの字形☃の**マフラーに色を付ける**」ための読みにくくないTypstモジュールを**つくりました**:smiley:

* [scmuffler ― ゆきだるま☃のマフラーに色を付ける][gist-scmuffler]（Gist/zr-tex8r）

一応パッケージ(package)の形にしてあるので、ローカルでインストールすればパッケージとして読み込めます。

```
// ローカルインストールしてパッケージとして読み込む場合
// ※バージョン指定は実際のものに合わせる
#import "@local/scmuffler:0.3.0": *
```

ですが、単純にscmuffler.typのファイルをカレントディレクトリに配置してモジュールとして読み込むのでもいいでしょう。

```
// カレントに置いてモジュールとして読み込む場合
#import "scmuffler.typ": *
```

:::note info
sc1Typstと同様に「[原ノ味明朝][haranoaji]」フォントをTypstで利用可能な状態にしておく必要があります。（一番手っ取り早い方法はフォントをOSにインストールすることです。）
:::

### おやくそく

この節に挙げる例では和文用の最低限のレイアウト設定として次のモジュールを使用します。

```text:layout.typ
// 和文用の最低限のレイアウト設定
#let doc(body) = {
  set page(paper: "a5")
  set text(
    lang: "ja",
    font: "Harano Aji Mincho", // 全面的に"原ノ味明朝"を使う
    top-edge: 0.88em,
  )
  body
}
```

### キホン的な使い方

scmufflerパッケージは次の関数を提供します。

* `scmuffler(«色»)`［content値］： マフラーの部分を`«色»`（color値）で指定した色に変えて（原ノ味明朝の）“☃”の文字(U+2603)を出力する。

例えば次のような文書ソースを用意します。

```text:example-1.typ
#import "layout.typ": *
#show: doc // おやくそく
// モジュール読込
#import "scmuffler.typ": *

// "scmuffler(olive)"でolive色のマフラーの☃を出力
私は#scmuffler(olive)よりも#scmuffler(red)の方が好きです。
```

これをコンパイルすると、以下の出力が得られます。

|![image-6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14550/c879c835-a7ff-8902-efe1-c6fd041f16a6.png)|
|:-:|
|出力結果|

素敵:blush:

### マフラーの色を少しずつ変えていく

:::note info
これ以降はプログラミングを利用します :bow:
:::

Typstの`color.map`モジュールで提供される[​“turbo”のカラーマップ][turbo]の配色を利用してマフラーの色をグラデーションにしましょう。

```text:sample-2.typ
#import "layout.typ": *
#show: doc
#import "scmuffler.typ": *

// カラーマップからgradient値を作る
#let turbo = gradient.linear(..color.map.turbo)
// "sample()"で色を抽出できる
#for k in range(21) {
  scmuffler(turbo.sample(k / 20.0 * 100%).darken(25%))
}
```

|![image-7.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14550/9a157f8a-7dca-e6c1-1404-bf41625fc921.png)|
|:-:|
|出力結果|

素敵:blush::blush:

### 箇条書きの番号を色付きマフラーの☃にする

* `muffler-numbering(«色»,…)`［function値］： 色付きマフラーの☃を利用したカウンタ書式関数を生成する。マフラーの色は引数に指定したcolor値が順番に使われる。

```text:example-3.typ
#import "layout.typ": *
#show: doc
#import "scmuffler.typ": *

// muffler-numbering(<色>,...)
// 色付きマフラーの☃を利用したカウンタ書式関数を生成する.
// マフラーの色は引数に指定したものが順番に使われる.
#let muffler-numbering(..args) = {
  let colors = args.pos()
  (value) => {
    scmuffler(colors.at(calc.rem-euclid(value - 1, colors.len())))
  }
}

#set enum( // 赤・青・緑のマフラーを反復する
  numbering: muffler-numbering(red, blue, olive),
)
// Universeにあるパッケージの名前🙃
+ 粗体（cuti）
+ 口胡（kouhu）
+ 随機（suiji）
+ 条碼（tiaoma）
+ 投影（touying）
```

|![image-8.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14550/72acba71-f0a5-f0cf-b686-907b6defe20c.png)|
|:-:|
|出力結果|

素敵:blush::blush::blush:

## まとめ

TypstでもLaTeXでも何でもいいので、**イロイロな色のマフラー**を着けた**ゆきだるま**:snowman:が入った文書をドンドンつくりましょう！:blush:

[haranoaji]: https://github.com/trueroad/HaranoAjiFonts
[gist-sc1typst]: https://gist.github.com/zr-tex8r/281a2191373d0a11bbfe340e02a7596c
[gist-scmuffler]: https://gist.github.com/zr-tex8r/86d2f575f919c75a377832d528670a80
[nantokaconf2023]: https://x.com/zr_tex8r/status/1723185589758157082
[satysfiadvent]: https://qiita.com/zr_tex8r/items/56dee4364b05974d7a54
[shikano-conf]: https://github.com/k16shikano/presentations/blob/master/texconf2024/presentation.pdf
[snowman-1]: http://acetaminophen.hatenablog.com/entry/2015/12/13/080226
[texconf2024]: https://texconf2024.tumblr.com/
[turbo]: https://typst.app/docs/reference/visualize/color/#predefined-color-maps
[typst-app]: https://typst.app/
[typst]: https://typst.app/
[zr-conf]: https://github.com/zr-tex8r/SC-tools/blob/master/sc1typst/snowmanconf2023/slide.pdf