SVGフィルタで緊急事態宣言

blog.utgw.net

私も緊急事態宣言してみました。文字のオタクサイドです。

フォント

元ネタのNHKのやつはたぶんニューロダンです。

lets.fontworks.co.jp

これを使うとめっちゃそれっぽくなることは知られていますが、残念ながら私はLETSと契約していないので、Google Fontsで使えるNoto Sans JP Boldにします。

あと、なんとなくそれっぽくなりそうなので90%長体をかけました。

該当部分だけ抜き出すとこんなかんじです。Google Fontsはお手軽に使えて良いですね。

<head>
    <link rel="preconnect" href="https://fonts.googleapis.com" />
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
    <link
      href="https://fonts.googleapis.com/css2?family=Noto+Sans+JP:wght@700&display=swap"
      rel="stylesheet"
    />
</head>
#main {
      transform: translate(0, -50%) scale(0.9, 1);
      font-family: "Noto Sans JP", sans-serif;
}

縁取り

今回ブログを書いたのはここが面白かったからです。

もとの実装では-webkit-text-stroke-widthが使われているのですが、これを使って太い縁取りをしようとすると汚くなります。というのも、「縁を太らせる」処理なので本来の縁を中心に内側にも外側にも広がってしまうからです。

一般に、こういった縁取り処理は文字の外側にのみ縁をつけると綺麗になることが知られています[要出典]。私が縁取り文字をGimpで作るとしたら、

  1. 文字を白で入力
  2. 色域選択して文字の部分を選択範囲にする
  3. 選択範囲をnピクセル拡大
  4. 文字のレイヤーより下にレイヤーを追加し、そのレイヤーの選択範囲部分を黒で塗りつぶし

とします。

このように太い文字と細い文字を重ねる処理をCSSでできないだろうか……と調べたところ、CSSではないですがSVGフィルタというものでできることがわかりました。初めて聞いた機能でしたが、上に書いたような、いわば普通の画像処理が結構素直にできて面白かったです。

まず、該当部分のコードを示します。

    <h1 id="main">緊急事態宣言</h1>
    <svg
      version="1.1"
      xmlns="//www.w3.org/2000/svg"
      xmlns:xlink="//www.w3.org/1999/xlink"
      style="display:none;"
    >
      <defs>
        <filter id="stroke-text-svg-filter">
          <feColorMatrix
            in="SourceAlpha"
            type="matrix"
            values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0"
            result="color"
          />
          <feMorphology
            in="color"
            operator="dilate"
            radius="4"
            result="kakudai"
          ></feMorphology>
          <feMerge>
            <feMergeNode in="kakudai"></feMergeNode>
            <feMergeNode in="SourceGraphic"></feMergeNode>
          </feMerge>
        </filter>
      </defs>
    </svg>
#main {
      filter: url(#stroke-text-svg-filter);
}

突然HTMLにSVGが登場してやばげですが、これは特に画像として使うわけではなく、この中でフィルタを定義しているだけです。いわば、scriptタグの要領でsvgを書いています。そして、CSSでこのフィルタをDOM要素に紐つけています。

では、肝腎のフィルターの内容を順繰りに簡単に説明していきましょう。

入力画像を黒で塗りつぶしたコピーをつくる

<feColorMatrix
    in="SourceAlpha"
    type="matrix"
    values="0 0 0 0 0
            0 0 0 0 0
            0 0 0 0 0
            0 0 0 1 0"
    result="color"
/>

これは、「入力画像の透明度を保持した真っ黒な画像」を生成するフィルタです。feColorMatrixという色変換行列をかける操作を使っています。いきなりハードモードというかんじですね。

developer.mozilla.org

参考サイト(後述)を見て、最初は私も「なんなんだこの数字列は……」と思いましたが、やっていることはRGBAの4要素のベクトルのアフィン変換です。少し考えればわかります。

今回は、入力として"SourceAlpha"(入力画像の透明度のみ)をとっているので、A以外の入力はゼロですね。

つまり、入力された画像(今回の場合は文字の形)を(R,G,B,A)で塗りつぶした画像が

<feColorMatrix
    in="SourceAlpha"
    type="matrix"
    values="0 0 0 R 0
            0 0 0 G 0
            0 0 0 B 0
            0 0 0 A 0"
    result="color"
/>

で得られるというわけです。今回は完全に不透明な黒なので、(R,G,B,A)=(0,0,0,1)として、先ほどのような結果になりました。

なお、出力は"color"という名前で保存されています。

黒で塗りつぶしたコピーを広げる

「広げる」とはなんぞやという話ですが、Gimpの例でいう「選択範囲の拡大」に相当するものです。つまり、黒い文字を太くします。これが縁になるわけですね。

この操作はfeMorphologyのdialateでできます。

<feMorphology
    in="color"
    operator="dilate"
    radius="4"
    result="kakudai"
></feMorphology>

これをkakudaiという名前で保存しています。

元の文字と縁を重ねる

feMergeに下から順に書くと重ねられるみたいです。これでフィルタは完成です。

<feMerge>
    <feMergeNode in="kakudai"></feMergeNode>
    <feMergeNode in="SourceGraphic"></feMergeNode>
</feMerge>

文字サイズを調整する

微妙に面倒だったところです。

もとの実装では、文字サイズを画面幅をもとに決めています。一方、今回のSVGフィルタの「黒で塗りつぶしたコピーを広げる」ではradiusに固定値を指定しています。これでは当然、画面幅によって枠線の相対的な太さが変わってしまいます。

当然SVGフィルタの側に画面幅をもとにしたradiusを指定すればよさそうなものですが、その方法はよくわかりませんでした。

というわけで、

  1. 文字サイズを一度固定する(今回は80pxとした)
  2. それに対し固定幅の縁をつける
  3. 文字全体をCSSのtransformで拡大する

という方法をとることにしました。

が、残念ながら、CSSだけでtransformの倍率を画面幅と固定値の比をもとに指定する方法がわかりませんでした。というわけで仕方なくJavaScriptを書きました。

const setFontSize = () => {
    const winWidth = document.documentElement.clientWidth;
    const scale = winWidth * 0.8 / 80 / 6;
    document.querySelector("#main").style.transform=`translate(0, -50%) scale(${0.9*scale}, ${1*scale})`;
};
window.addEventListener('resize', setFontSize);

setFontSize();

これで望む動作が得られましたが、もうちょっとまともな方法がほしい気がします(たぶんあると思います)。

まとめ

SVGフィルタで文字の縁取りを良い感じにできました。なかなか楽しい機能だなという感想です。

ちなみに、背景画像は10年前に高校の遠足に行ったときに撮った東京の写真です(いいかんじの東京の写真がほかに出てこなかった)。

<2021/07/10 追記> 当初は文字サイズを80pxとしていましたが、スマートフォンなど「80px×6文字」が収まらない環境で2行になってしまっていたため、雑な処置として40pxを基本に変更しました。この記事の数字は面倒なので変更していませんが、貼っているGlitchのコードでは変更されています。 </追記>

参考サイト