↩️↩↩︎問題
なぜかカラーの絵文字になったり、モノクロになったりする問題。これを紐解く。
TL;DR
VS をつける。
VS がない場合、カラーになるかモノクロになるかは環境次第。
ユーザー入力を扱うなら、入力時に表示結果を見て VS を補完するのも一つの手。
サーバー側では、VS なしの文字が本来どちらを意図していたか判断できないため。
Tailwind の font-family も確認する。
デフォルト設定には Apple Color Emoji などのカラーフォントが含まれており、VS なしの文字がカラーで描画されやすい。
font-variant-emoji も確認する。
描画方法を指定できる。環境をそのまま尊重したいなら normal がよい。
VS (variation selector)は、特定の文字の「見た目のバリエーション」を指定するための後続コードポイント。"異字体セレクタ" とも言う。
たとえば ↩️ (U+21A9) 自体は一つだが、その後ろに VS16(U+FE0F)を付ければ「絵文字として表示せよ」、VS15(U+FE0E)を付ければ「テキスト(記号)として表示せよ」という意図をフォントやレンダラに伝えることができる。
まず、見出しの絵文字について。
↩️
U+21A9 + U+FE0F (VS16)
絵文字として表示したい、という指定。カラー絵文字になる。
↩
U+21A9 単体
指定なし。カラーになるかモノクロになるかは環境次第。
↩︎
U+21A9 + U+FE0E (VS15)
テキストとして表示したい、という指定。モノクロ記号になる。
この真ん中のVS無しの場合が問題
カラーの絵文字もモノクロの絵文字も持っているもの、例えば ✈/✈️ ©/©️ とかもあるが、これらが「VS なしのときにどちらが描画されるか」はかなり複雑。
一応 Unicode 側に推奨の表示形(推奨プレゼンテーション)があるが、デバイスや OS に大きく依存する。また、ロケールなどの要因が影響する場合もあり、完全に一意に予測することは難しい。
詳しくはこちらがとてもわかり易かった。
CSS 側の話
Web では font-variant-emoji である程度制御できる。
たとえば font-variant-emoji: text; を指定すると、対応環境では ↩ をテキストとして表示させやすくなる。
ただし、これだけでは足りない場合がある。
実際にはフォントファミリの影響も強い。
今回の原因
モノクロの絵文字を選択しているのに、何故かカラー絵文字が描画されてしまう状況。
https://scrapbox.io/files/69ef888bfc4464f697d29721.png
よくよく調べてみると、原因は Tailwind のデフォルトの font-family だった。
https://scrapbox.io/files/69ef87dbfc4464f697d29613.png
code:css
ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'
よく見るとカラー絵文字が指定されている。これだと、VS なしの ↩ もカラーフォントで拾われる。
対策は単純にtailwind themeでフォント指定を変更する
code:css
@theme {
--font-sans: ui-sans-serif, system-ui, sans-serif;
}
IME の違い
入力時点でも差がある。
当方の環境で確認した限りでは、macOS 標準 IME は、モノクロの絵文字を選択した時に VS15 を付けていた。一方で Google IME は VS を付けず、U+21A9 単体。この場合は、最終的な見た目がフォントや環境に委ねられる。
実運用で困る点
ユーザー入力をそのまま受けると、VS なしの文字が送られてくることがある。
この状態のままサーバーで OGP 画像などを生成すると、その文字が本来カラー絵文字のつもりだったのか、モノクロ記号のつもりだったのかが分からない。
なので、入力時のクライアント側で実際の描画結果を見て、必要なら VS を補完してから送る、という方法がありうる。
実装はシンプルにcanvasに描画してみて環境でのフォントレンダリングをチェックする。canvasでのフォント指定を入力欄と一致させないと意味がなくなるので要注意。
code:js
function isEmojiPresentation(char) {
const canvas = document.createElement("canvas");
canvas.width = 64;
canvas.height = 64;
const ctx = canvas.getContext("2d");
ctx.font = "48px serif"; // テキストフィールドと同じフォントを指定
ctx.fillText(char, 8, 52);
const { data } = ctx.getImageData(0, 0, 64, 64);
const colors = new Set();
for (let i = 0; i < data.length; i += 4) {
if (datai + 3 === 0) continue; // 透明ピクセルを除外 colors.add(${data[i]},${data[i + 1]},${data[i + 2]});
}
// カラー絵文字なら複数色、テキスト表示なら単色(or グレースケール)
return { char, colorCount: colors.size, isColor: colors.size > 2 };
}
まとめ
見た目を固定したいなら VS をつけるのが確実で、VS なしは環境依存になる。
それとは別に、最近は Tailwind のデフォルト font-family がそのまま効いていて、意図せずカラーフォントが選ばれてしまうケースもわりとありそうだった。要注意。