iOS Safari ではXHRで巨大なHTMLを読み込めない事がある

古き良き、サーバーサイドレンダリングなマルチページで一部分だけ動的に書き換える際に、HTML全体を非同期で取得して必要なDOMだけ差し替えるような実装をすることがある。

たとえば WordPress サイトなんかで、サーバーサイドは変更せずに簡易的な非同期遷移を実装しようと思ったらこういう実装になる。

const xhr = new XMLHttpRequest();
xhr.open('GET', 'http://192.168.x.x/hoge/fuga', true);
xhr.responseType = 'document';
xhr.addEventListener('readystatechange', () => {
    if (xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) {
        const html = xhr.response.querySelector('.hogehoge-selector').innerHTML;
        container.innerHTML = html;
    }
});
xhr.send();

注目スべきはここ

xhr.responseType = 'document';

document を指定することで xhr.response がDocumentで返ってくるので、いきなり querySelector なんかを使うことができるのだ。

こんな感じで実装されていたわりと古いコードがあったのだが、iPhone実機でエラーに遭遇した。

TypeError: null is not an object (evaluating 'xhr.response.querySelector')

readystatechange リスナー内の xhr.responsenull だというのだ。
xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200 という条件式の中にあるにも関わらず。

Webインスペクタのネットワークパネルでレスポンス内容を見ると正しくHTMLがレスポンスされているのも確認できる。

しかもこのエラー、毎回出るのではなく、5回に1回程度の割合で出るのでたちが悪い。

試しにDocumentではなく文字列で取得してみる

    if (xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) {
        console.log(xhr.responseText);
    }

InvalidStateError: The object is in an invalid state.

xhr.responseText にアクセスすらできない状態で、エラー内容もよくわからない。

エラー文でググってみると、どうやらガーベジコレクションが行われたオブジェクトに対してアクセスしたときに出るエラーのようだ。まだ使ってないのにGCが発火してしまったらしい。

今回この事象に遭遇したのは2000行超あるかなり巨大なHTMLだったので、試しに500行くらいまで減らしてみると、案の定エラーが出ることはなくなって、正常動作が確認できた。

少しずつ行数を増やしていくと、徐々にエラーになる確率が増えていくような感じがあるので、やはりメモリ管理にバグがあるらしい。

手元の環境では、下記で発生することが確認できた

  • iPhone X : iOS 15.0, 15.0.1
  • iPad Pro 11インチ (第1世代) : iPadOS 15.0

Mac Safari や iOS シミュレータでは再現しなかったが、母艦の搭載メモリや起動しているアプリなど、条件によっては発生するんじゃないかとおもう。

行った対処法としては、

xhr.responseType = 'document';

これを使うのをやめて、textで取得し、 DOMParser なりなんなりで自分でDocumentに変換してやる。

xhr.responseType = 'text';
xhr.addEventListener('readystatechange', () => {
    if (xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) {
        const parser = new DOMParser();
        const doc = parser.parseFromString(xhr.response, 'text/html');
        console.log(doc);
    }
});

もしくはレガシーコードは窓から投げ捨てて、素直に fetch で書き直してればこんなバグには遭遇しないのだ。

fetch('http://192.168.x.x/hoge/fuga')
    .then(response => response.text())
    .then(text => {
        const parser = new DOMParser();
        const doc = parser.parseFromString(text, 'text/html');
        console.log(doc);
    });

いまどきXHRなんて使ってんのが悪い。

TypeScript+Rollupでビルドが終わらない

RollupでTypeScriptのビルド設定を書いていたら、なぜかrollupのビルドコマンドがいつまで経っても終わらず、ctrl+cせざるをえない状態になり、原因調査に時間がかかったので備忘メモ

症状としてはこんな感じ

ビルド自体は終わっている(標準出力)のに、プロンプトが返ってこない

エラーも何も出ないのでなんの情報もなく、途方に暮れた挙げ句は神に祈りながらMacの再起動までしてみたけどダメでした 🤷‍♂️

どうやらtypescriptの4.4.xにバグがあるみたいで、4.3.xや4.5.0のデイリービルドなら起きないようだ。

すでに PRが作られて おり、4.4.3でリリースされるみたいなのでもう少し待ってみるか

ディスプレイを接続していないWindowsにChromeRemoteDesktopで接続すると重くなる場合はHDMIダミープラグを挿すと良い

自宅のWindows環境はファイルサーバ・DLNAサーバとして常時起動させています。

Windowsでなにか作業をすることはほとんどないので、いっそのことディスプレイを接続するのをやめることにしました。

たまに触るときはChrome Remote DesktopでスマホやMacからアクセスしていたのですが、どうもディスプレイを接続していない環境にChrome Remote DesktopでアクセスするとOS全体のパフォーマンスが著しく低下するようです。

原因はよくわかりませんが、ディスプレイを繋いでいれば快適に動くので、試しにHDMIダミープラグを買って挿してみると案の定快適に動作しました。

なんのために使う製品なのかわかってなかったですが、こういう使い方もあるんですね。

Amazonレビューを見ると、解像度の高いディスプレイを持っていないけど作業スペースを広く使いたい場合に、4Kのダミープラグを指し、あえてリモートで接続することで疑似4Kで作業できるため、自宅環境が整っていないリモートワーカーに人気があるらしい。

MacBookで4K120Hzを出力できる組み合わせ

現行のMacBookシリーズは高解像度かつ高リフレッシュレートな映像出力が不安定で、マシン・OS・接続方法の組み合わせで出力できたりできなかったりする。
それを備忘録的にまとめてみます

① MacBook本体

そもそもの話なのだけど、MacBookやiMac本体のディスプレイは解像度こそ高いがリフレッシュレートは60Hzです。
FHD@120Hz程度ならどんなMacでも出力可能ですが、4K@120Hzとなると4倍の演算能力が必要になるのでマシン自体が限られてくる
具体的に言うとIntel Graphicsでは演算能力が足りず出力不可能です

4K120Hz出力ができるMacBookは、

  • M1 Mac
  • Radeonなどのグラフィックチップを積んでる Intel Mac

M1か16インチのIntelMacを選んでおけば間違いないです

6K@60Hzや8K@30Hzの出力が可能なグラフィック能力があれば理屈では4K120Hzも出力できるはずで、MacBook以外もこの理屈でOK
eGPUでもイケるみたいです

② MacOS

ここに1つ目の落とし穴があります

M1 Mac

BigSurで出力可能。それ以外に選択肢もないので迷うことはありません

Intel Mac

BigSurでは4K@120Hz出力ができません
おそらくソフトウェア的なバグがあるので、現時点ではCatalinaを使うことをオススメします

③ 接続インターフェース

ここにはThunderboltやUSB-Cの複雑すぎる規格とMacの中途半端な実装に落とし穴があります

4K@120Hzの転送をするために注目すべき箇所は

  • Thunderbolt3もしくはUSB4ケーブル
  • DisplayPort 1.4対応 (= HBR3対応)
  • ケーブルの端子は USB-C to DisplayPort

HBR3と記載のあるThunderbolt3であればスペックは足りているのですが、Macのソフトウェア的な問題なのか、 ディスプレイ側はDisplayPortでないと4K@120Hzで出力ができません
Apple純正のThunderbolt3ケーブルでも無理でした。
(M1 Macでは試せていないので、両端がUSB-CなThunderbolt3ケーブルでも出力可能かもしれません)

菅沼環境ではこのケーブルで4K@144Hzを確認してます
https://www.amazon.co.jp/gp/product/B083V6VMNM/


上記をすべて満たした環境で、対応したディスプレイに接続すればOK

システム環境設定

[ディスプレイ]

optionを押しながら 解像度の[変更]

[低解像度モードを表示] にチェック

とすると、リフレッシュレートの選択肢が現れるはずです

DisplayMenu等のアプリでも設定可能ですが、なぜか8bitカラー(ARGB8888)になってしまうので、システム環境設定から変更することをオススメします

ランチに行く人を勝手に選出するGoogle App Scriptを作った

弊社では、コロナ禍以前からその日に最もパフォーマンスの出る場所で働くことをルールとして環境を整えているので、あまり事務所に人が集まることはありません。

自宅でも事務所でも、同じように仕事ができることは、これができない方々からすれば良いことのように思うかもしれませんが、各自宅に環境が整いすぎると出社する理由がなくなってしまい、万年リモートのようになってしまう人が一定数現れます。

リモートでは業務上の連絡はもちろん、わからないこと、知りたいこと、聞きたいことはチャットツールでもオンライン会議ツールでも、最適なものを使えば不自由なくコミュニケーションできますが、隣の席の人がどんな仕事をしているのかはわかりませんし、斜め後ろの仲良しの2人の雑談が耳に入ることもありません。

もし自分の仕事と無関係な情報だったとしても、自分と違う環境で生きている人間の話は、新たな興味を生むキッカケになり、仕事の幅や人生の歩みをわずかに変える可能性があります。

ある程度社会人経験を積んだオッサンだけの会社であれば事務所の重要性なんぞほとんどないと思うのですが、新卒を含む若年者にとっては、せっかく組織で働くのであればこういうメリットは大いに教授すべきだと思うわけです。

そんな万年リモート社員を多く抱える弊社でなんとなくアンケート調査を行ったところ、出社がイヤで万年リモートなのではなく、出社する理由がないだけであることがわかりました。

そこで、出社するキッカケづくりをするため、一緒にランチに行く人をGoogleカレンダーにランダムでアサインするGoogle App Scriptを作ってみることに。

要件は

  • このスクリプト専用のGoogleカレンダーを作成し、毎週月曜日にランチの予定を作る
  • 予定を作成するのは可能であればサービスアカウントを使いたい
  • ランダムで選出した社員3名を前述の予定に招待する
  • 毎月10日に来月の予定を自動作成するようトリガーをセットする

以下gs全文です。

こちらの記事を参考にさせて頂きました。

サービスアカウントの作り方なんかも説明するの面倒なので上記記事から辿ってください。

途中サービスアカウントを扱うためのGSAppというライブラリを使っていますが、メンテナンスされておらずライブラリとして読み込むことができなかったので元のソースを別のgsファイルとしてコピペしています。

const MEMBER_NUMBER = 3;
const USERS = [ // 社員のメールアドレス(Googleアカウント)を列挙しとく
	'staff001@nasbi.jp',
	'staff002@nasbi.jp',
	'staff003@nasbi.jp',
	'staff004@nasbi.jp',
	'staff005@nasbi.jp',
];
const SCOPE = [
  'https://www.googleapis.com/auth/calendar',
  'https://www.googleapis.com/auth/calendar.events', // たぶんいらない
  'https://www.googleapis.com/auth/admin.directory.resource.calendar', // ユーザー招待するのに必要?いらないかも
  'https://www.googleapis.com/auth/gmail.send' // 招待メールを飛ばすので必要かと思ったけど動いてない
];
const CALENDAR_ID = 'hogefugapiyo@group.calendar.google.com';

function main() {
  // カレンダーのスコープを指定
  const serverToken = new GSApp(jsonKey.private_key, SCOPE, jsonKey.client_email);

  // トークンの取得
  const tokens = serverToken.addUser(USERS[0]).requestToken().getTokens();

  const events = generateNextMonthEvents();
  events.forEach((event) => {
    insertCalendar(event, CALENDAR_ID, tokens[USERS[0]].token)
  });

  console.log('done');
}

function insertCalendar(eventData, CALENDAR_ID, token) {

  const date = Utilities.formatDate(eventData.date,'JST', 'yyyy-MM-dd');

  const attendees = eventData.members.map((email) => ({ email }))

  const payload = {
    start: { date },
    end: { date },
    summary: 'ランチ行くぞ ٩(ˊᗜˋ*)و',
    attendees,
  };

  // リクエストの設定 
  const fetchOptions = {
    method: 'post',
    payload: JSON.stringify(payload),
    contentType: 'application/json',
    headers: {
      Authorization: `Bearer ${token}`,
    },
    muteHttpExceptions: true,
  };

  //リクエストURLを作成  
  const url = `https://www.googleapis.com/calendar/v3/calendars/${CALENDAR_ID}/events`;

  try {
    const res = UrlFetchApp.fetch(url, fetchOptions);
    Logger.log(res)
  } catch(e) {
    // 例外エラー処理
    Logger.log('Error:')
    Logger.log(e)
  }
}

function generateNextMonthEvents() {
  const events = [];

  const today = new Date();
  const firstDay = new Date();
  firstDay.setMonth(today.getMonth() + 1);
  firstDay.setDate(1);

  const thisMonth = firstDay.getMonth();
  const tmpDate = dateCopy(firstDay);
  while (tmpDate.getMonth() === thisMonth) {
    if (tmpDate.getDay() === 1) { // 月曜日は1
      events.push({
        date: dateCopy(tmpDate),
      });
    }
    tmpDate.setDate(tmpDate.getDate() + 1);
  }

  let tmpUsers = [];
  events.forEach((event) => {
    event.members = [];
    for (let i = 0; i < MEMBER_NUMBER; i++) {
      if (tmpUsers.length === 0) {
        tmpUsers = arrayShuffle([...USERS]);
      }
      while (event.members.includes(tmpUsers[0])) {
        tmpUsers = arrayShuffle(tmpUsers)
      }
      event.members.push(tmpUsers.shift());
    }
  })

  console.log(events);
  return events;
}

/**
 * 引数のDateを複製する
 * @param {Date} date
 * @return {Date}
 */
function dateCopy(date) {
	return new Date(date.getTime());
}

/**
 * 配列をシャッフルする
 * @param {Array} origin
 * @return {Array}
 */
function arrayShuffle(origin) {
	const array = [...origin];
	for (let i = array.length - 1; i > 0; i--) {
		const r = Math.floor(Math.random() * (i + 1));
		const tmp = array[i];
		array[i] = array[r];
		array[r] = tmp;
	}
	return array;
}

同じ人が毎週招待されたり、招待されない人がいないよう、単純なランダム選出ではなく満遍なく招待が届くようにしてみた。

GASの発火トリガーはこんな感じ


作ってみて困ったのは、2つ。

どうせならアカウントに依存しないようサービスアカウントを予定の作成者としたかったのだけど、他のアカウントを予定に招待する場合は実物のGoogleアカウントを偽装して予定の作成をする必要があったこと。

どうやらスパム対策でこのような仕様になったみたい。

偽装する必要があるなら最初からサービスアカウントなんて使う必要なかったなあと思ったり。

もうひとつは招待メールが飛ばないこと。

これは原因がわからなかったのだけど、もしかしたらこれもスパム対策なのかもしれない。

ともあれ最低限の要件は満たせたと思うので、来月から運用を開始します。

もちろんこれはあくまでキッカケづくりであり義務ではないので、気分じゃなかったら参加しなくてもいいし、ふたり以上集まればランチ代は領収書を切ってくれて構わないです

義務や個人の負担はチリツモでモチベーション低下に繋がりますからね。

事務所に二酸化炭素濃度センサーを導入した

弊社事務所の入居する建物は、築50年近くになるマンションです。

オフィス向けに内装をリノベーションこそされているものの、壁がコンクリートに直接壁紙を貼ってあるような状態なので、冬場はエアコンなんかじゃ全然暖まりません。

幸いにもガスコンセントが設置されていたので、入居当初からガスファンヒーターを設置して越冬しています。

ガスファンヒーターはエアコンとは比にならないくらい暖かく、ガス燃焼の副産物として加湿機能も付いているので、とても快適な冬を過ごせています。

ただし、やはり空気中の酸素を使って燃焼させる仕組み上、定期的な換気が欠かせません。

最長でも8時間しか連続稼働できない仕様になっていますが、使い方を間違えると一酸化炭素中毒で死亡事故にも繋がります。

そこまでじゃないにしろ、しばらく稼働して酸素濃度が減ってくると頭痛や軽い吐き気を催すなどの体調不調が起きるので、そのタイミングで空気の入れ替えをする運用をしていたのですが、体調不調がおきてから行動するのはどう考えても危険なので、できれば事前に換気のタイミングを知ることのできる手段を用意する必要性を常々感じていました。

そこでIoTの力を借りるべく、Smart Indoor Air Quality Monitor をで購入。

Netatmoは、スマートホームデバイスを製造しているフランスの会社です。

日本で取り扱いはありませんが(以前は代理店があったみたいだけど、なくなったっぽい)、AmazonUKで購入可能で、日本国内へ送ってくれます。

本体は想像のふた周りは小さく、iPhoneXと並べてもそれほど大きさに差はありません。

付属のACアダプターはBFタイプと呼ばれる形で、日本で使うには変換プラグが必要になりますが、本体差込口はUSB type-Bなので適当な給電環境で賄えることが多いと思います。MacbookProとのUSB接続でも問題なく動きました。

センサーは4つあって

  • 気温
  • 湿度
  • 二酸化炭素濃度
  • 騒音レベル

が専用のアプリから確認できます。

二酸化炭素濃度に関しては、ExcellentやBadなど、4段階程度の閾値があり、それを跨ぐタイミングでアプリに通知が来る仕組みになっています。

菅沼個人の感覚では5000ppmを超えたあたりから(センサーの上限が5000ppm)明らかな体調不調が起こるので、それに達する前に換気するよう心がけることができるようになりました。

できれば「3000ppmを超えたら通知」みたいな設定を自由に指定できたら良かったのですが、決められたタイミングで通知されるものを受け取るか否かしか決められないのが少し残念なところ。

連携できるアプリ(デバイス)数に制限がないので、スタッフ全員がそれぞれ通知を受け取れ、各々のタイミングで換気ができるのは良いところでした。

Smart Indoor Air Quality Monitorは、Weather Stationの廉価版のような製品で、こちらにはより詳細なWebインターフェースがあるみたいなので、もしかしたらこっちなら詳細な通知設定ができるのかもしれません。


二酸化炭素濃度が高い状態だと、頭痛や吐き気の他にも、眠気や集中力の低下に繋がるらしいので、見に覚えがある方は換気を心がけるか、このようなセンサーを導入してみると面白いかもしれません。

ちなみに、弊社環境ではガスファンヒーターを点火してから2時間程度で5000ppmを超えます。

ガスファンヒーターを付けずとも、35平米程度の部屋に2〜3人いるだけで二酸化炭素濃度はグングン上がっていきます。

酸欠状態は思ってたよりも身近にあって、チーム全体の仕事のパフォーマンスに直結するのであまり蔑ろにできないというのがこの件の気づきです。

Web制作の現場で使える最強ディスプレイ

主にフロントエンドエンジニアやウェブデザイナーはどんなディスプレイを使うべきなのか、改めて今という時代を鑑みて考えてみた。

今使っているディスプレイ

今、弊社ではLGの24UD58-Bを人数分買って各自で使っている。

物理的なサイズや解像度は満足しているし、高級なリファレンスモニターと見比べたこともないので色に関しても劣っていると感じたことはない。

このスペックで3万円程度で購入できるのであればとてもいい製品だと思っている。

ユーザーの使っているディスプレイ

今の時代、普通の人はみんなスマホを持っている。

調べていくうちに、スマホのディスプレイは実は制作現場で使っているディスプレイより遥かにハイスペックであることがわかってきた。

制作現場よりエンドユーザーのほうがスペックが高い時代なんてあっただろうか?今の状況おかしくないか?

どう考えても良くないだろう。

制作現場で使える最強のディスプレイを探す旅に出ることにした。

高画質とは

そもそも良いディスプレイとは何なのかを知る必要がある。

こちらの高画質についての解説を見るとわかりやすいのだが、
https://www.eizo.co.jp/eizolibrary/color_management/hdr/

大きく分けて5つのスペックで画質の良し悪しが決まるという。

  • 解像度
  • ビット深度
  • フレームレート(リフレッシュレート)
  • 色域
  • 輝度

ディスプレイのトレンド

現状のハイエンドに位置するスマホは、以下のようなスペックが主流だ。

  • Retinaなどの高解像度ディスプレイ
  • 10億色表示可能
  • 90Hzや120Hzなど高リフレッシュレート
  • Display P3 色域対応
  • 有機EL

弊社で使っているディスプレイは解像度と色深度こそ高いものの、それ意外は完全にスマホに劣っている。

MacbookProのディスプレイでさえ、P3に対応しているくらいで、有機ELで高リフレッシュレートなスマホには劣っていると言わざるを得ない。

制作現場として必要な要件

5つのスペック項目ごとに要求スペックを決めていく。

1. 解像度

4K がほしい。

スマホは解像度こそ高いがドットバイドットでコンテンツを表示することはあまりないので、作業スペースという意味ではそれほど広いものは必要ではない。
ただ、やはりスマホユーザーがRetina前提なので制作現場がRetina相当でないのはおかしいと思う。

また、一般的なPCユーザーはフルHDが主流だとすると、フルHDが作業スペースでは狭いだろう。
2K程度の解像度をドットバイドット以上で表示できるディスプレイが必要なら、やはり最低でも4Kはほしい。

ドットバイドットで表示する時代はとっくの昔に終わったのだ。

2. ビット深度

できれば True 10bit がいい。
妥協して8bit+FRCでもいいかもしれない。

最近のディスプレイは10億色を謳っているものがほとんどなので、少なくとも8bit+FRCではあるみたい。

3. リフレッシュレート

120Hz 程度はほしいところ。
iPad Pro シリーズがProMotionという名前で120Hzディスプレイを搭載しているので、これを基準としたい。
Pixelを始めとするAndroid機も高リフレッシュレートなデバイスがスタンダードになりつつある。
おそらく遠くない未来でiPhoneシリーズにもProMotionが搭載されるだろう。

Webでアニメーションを作る際に、一般的に60fpsを維持すると良いとされているが、今後その基準を120fpsに引き上げて作っていきたい。

4. 色域

Apple様が提唱する Display P3 への対応。

AppleがsRGBを置き換える次世代の規格として推してるの色域なので、iPhoneからMacbookまで、Apple製品はすべて対応を謳っている。
Androidもこれに追従する形でOS側が対応を始めたので、デバイスもP3対応機種が増えつつある。

100%は難しいとしても、最低でも95%以上でなるべく高い数値だととても良い。
価格との兼ね合いになりそう。

5. 輝度

ダイナミックレンジが大きいに越したことはないが、Web制作の現場では、それほど重要ではないかなと思っている。
今回は 一般的な値であれば十分 という要件にしよう。

黒の表現という意味では、現状スマホのスタンダードになっている有機ELに勝るものはないので、可能なら制作環境も有機ELにしたいのだが、大きい有機ELディスプレイは量産されておらず、現実的に買える値段で売っていないので諦めざるを得ない。
今年に入ってようやくLGから発表された有機ELのPC用ディスプレイが唯一選択肢になりえるかもしれない、というレベル。
マイクロLEDなどの新しいアプローチの製品も考案されつつあるので、もう少し手に入れるには時間が必要のよう。

製品を探す

要求スペックをまとめるとこうなる。

  • 解像度:4K
  • ビット深度:10bit
  • リフレッシュレート:120Hz
  • 色域:P3 95%

これを基準に、ジャンル別にディスプレイを探してみた。

リファレンスモニター

リファレンスモニターと呼ばれるEIZOやSONYの高価なディスプレイは正しい色を表示することに関して非常に長けていて、これはこれで価値のあるものだと思うのだけど、Web制作の現場ではオーバースペックか。

  • 色域すごい
  • キャリブレーション機能がついていたりする
  • リフレッシュレートは普通
  • 高価

映像や写真のクリエイター向けに作られていることが多く、高価な割には高リフレッシュレートな製品は作られていないみたい。

ゲーミングモニター

一般的なゲーミングモニターと呼ばれるジャンルのディスプレイの特徴は、

  • 応答速度が早い
  • リフレッシュレートが高い
  • フルHD解像度
  • 色域は非公開(よいものではない)

リフレッシュレートが高いのは良いことだが、解像度がフルHDに抑えられているのは必要とされていないからだろう。
リアルタイムレンダリングを主とするゲーム用途では高リフレッシュレートな4Kは出力側のマシンスペック的にまだまだ難しい。
具体的に言うとPS5レベルの次世代ハードウェアが必要になる。
今後ハードウェアに合わせてよりハイスペックな製品が登場するのだろう。

応答速度は制作現場にはあまり重要ではない。

どうやら需要がないらしい

リファレンスモニターやゲーミングモニターを調べてみて感じたのは、要求スペックに合う方向性の製品がほぼ存在しないということ。

映像制作現場では高リフレッシュレートは不要だし、ゲーミングモニターでは高解像度と広色域は不要なのだ。

見つけた理想のディスプレイ

Eve Spectrum

クラウドファンディングで資金調達したフィンランドのeveというスタートアップの製品。

  • 4K
  • 8bit+FRC
  • 144Hz
  • P3 98%
  • DisplayHDR 600

色深度こそなんちゃって10bitだが、それ以外は完璧に要件を満たしている夢のような製品。
価格も709ドルと、AppleのPro Display XDRやDELLのUP2720Qなんかと比べても圧倒的に安い。

さっそくポチってみた。
3月頃に注文したら4月末に届くらしいが、この会社は以前の製品でとんでもない遅延をしたらしいので、気長に待つことにする。

もし無事に届いて気が向いたらレビューでもしてみようと思います。

ちなみに

後で発見したのだが、EveのSpectrumは、おそらくLGの27GN950-Bと同じパネルなのであろう。

LG 27GN950-B

スペックがほぼ同じだ。

ただし、LGのほうが価格がちょっと高く、背面がレインボーに光る。
個人的には多少のリスクを抱えてもSpectrumを選んだが、好みにあうのであればLGを選んでもいい買い物だと思う。

CSSでフォントサイズの変数名にSI接頭辞を使ってみたらどうだろう

これは今に始まったことじゃないですが、JSにしろCSSにしろ、変数名ってめちゃくちゃ悩むんですよね。

今日はCSSのフォントサイズを変数化する際にSI接頭辞を使ってみたらどうだろうか、という提案をさせていただきたい。

フォントサイズの値の特徴として、下記のようなものがあると思います

  • 数値型
  • 基準となる値があり、それより小さいものと大きいものに分けられる
  • 値そのものより、他の値より大きいのか小さいのかが重要(イメージできるので)
  • せいぜい5〜8種類程度になるはず

要するに 数値型で相対的な大きさがなんとなく想像できるような変数名 だと良い変数だと思うわけです。

そこでこちらをご覧ください

$font-size-micro: 12px;
$font-size-milli: 14px;
$font-size-base: 16px;
$font-size-kilo: 18px;
$font-size-mega: 24px;
$font-size-giga: 32px;

基準である16pxをベースに、それより小さいものをミリやマイクロ、大きいものをキロ・メガ・ギガとすることで、相対的な大きさを想像することができる変数名とすることができたと思います

試しにこの変数名を、あえて分かりにくいセレクタで使ってみるとこんな感じになります。

.headline {
  font-size: $font-size-kilo;
}
.date {
  font-size: $font-size-milli;
}
.value {
  font-size: $font-size-giga;
}

フォントサイズの大小が明確になるだけで、随分と要素の重要度がCSSを見ただけで伝わるのではないかと思うわけです。

しかも、この程度のSI接頭辞ならエンジニアではなくても、なんなら小学生でも直感的にわかるという嬉しさ。

中間サイズの追加に弱いという欠点はあるものの、デザインシステムがカチッとしている状況であれば、十分使えるのではないでしょうか。


ちなみに、これを考えているときに思いついた別案ですが、むしろ日本語で曖昧な変数名作っちゃえばもっとわかりやすくて汎用的になるのではと思ったりしました

$font-size-結構小さい: 12px;
$font-size-ちょっと小さい: 14px;
$font-size-基準: 16px;
$font-size-ちょっとデカい: 18px;
$font-size-まあまあデカい: 24px;
$font-size-相当なデカさ: 32px;
$font-size-アホほどデカい: 64px;

これなら中間サイズが追加されても表現次第でどうにでもなるので最強です。優れたエディタなら補完もしてくれるので表記ゆれなんて気にしなくて良い。日本語すごい!

SVG要素でaltの代わりになるもの

SVGを表示するにはいくつか方法があります。

pngやjpgと同じように <img> を使って表示する場合はいつも通り代替テキストとしてalt属性を使えばよいのですが、SVGスプライトや vue-svg-loader を使うなどで <svg> をHTMLにインラインで書き出したい場合もあると思います。

<svg> には代替テキストの機能は存在しないので、代わりとなる機能がないか探してみました

<title> を使う方法

SVGファイル内に <title> を入れておくことで、マシンリーダブルにすることができます。

ただしこの方法には、

  • カーソルを合わせるとツールチップが出てしまう
  • .svg ファイル内に存在することになるので、管理がしづらい

などの取り回ししづらい部分があり、imgのaltと比べてもなんかいまいち。

WAI-ARIAを使う方法

そこでおすすめしたいのがこちら

<svg> に対して role="img" aria-label="テキスト" を付ける方法です

<svg role="img" aria-label="画像の説明" ...>
  ...
</svg>

この方法は MDNのimgロールのページ でも言及されていました。

ARIA属性は、付与できる要素に制限がないため、例えば複数の画像をまとめてひとつのものとして認識してもらいたい場合などにも使うことが出来ます。

<div role="img" aria-label="画像の説明">
  <svg ...>
    ...
  </svg>
  <svg ...>
    ...
  </svg>
</div>

<title> を使う方法と比較して管理のしやすさや組み込み方自由度が高く、非常に有用だと思います

文字を思い通りの大きさと位置で表示するCSS

基本的に文字というものは、大文字・小文字など文字の種類によって目に見える大きさ(高さ)が異なります。

例えばCSSで font-size を100pxに指定した場合でも、文字がピッタリ100pxで表示されるわけではありません。

これをCSSで思い通りに指定する方法を調べてみました。

(欧文フォントの場合)

欧文フォントの基礎知識

アルファベットでは、文字の下端( y などの下に飛び出る小文字を除く)となるベースラインを基準に、デザイン上の指標となる高さがいくつか存在します。

  • アセンダ:ベースラインから bd など小文字で上に飛び出る部分の上端までの距離
  • ディセンダ:ベースラインから jy など下に飛び出る文字の下端までの距離
  • エックスハイト:ベースラインから x など、小文字の上端までの距離
  • キャップハイト:ベースラインから大文字の上端までの距離。アセンダより少し下に位置することが多い

※こちらの図がわかりやすいです http://w3.kcua.ac.jp/~fujiwara/infosci/font.html

例えば Arial のメタデータを解析してみると、下記の値を発見できます

  • ascender: 1854
  • descender: 434
  • sxHeight: 1062
  • sCapHeight: 1467

数字の大小はさておき、これらの値でまず意識しなければならないのは単位です。

実はこの数字、単位はピクセルでもミリメートルでもありません。
各フォントが em-square という基準値を決め、それに対する相対値として定義されています。

これもメタデータを見てみると発見できます

  • unitsPerEm: 2048

要するに Arial の場合は、2048を基準に、それよりどのくらい大きいのか、小さいのか、という相対的な数字なのです

文字サイズ16pxのxの大きさ

そしてこの em-square こそがCSSの font-size と直接関わってくる部分。

例えば font-size: 16px; とする場合、xの高さは

var xHeight = (1062 / 2048) * 16px

約8.3pxとなります。

また、逆算的にキャップハイトが100pxになるためのfont-sizeは

var fontSize = 100px / (1467 / 2048)

約139.6pxになります。

font-size は、実は スケール感をふんわり指定しているだけ で、必ずしもその大きさで文字が表示されるわけではない、ということが分かって頂けたかと思います。

文字の表示位置とline-height

ここまでで表示したい大きさのfont-sizeは見いだせるようになったと思いますが、実際にこれをウェブのレイアウトで使うためには、表示位置も制御できねば意味がありません。

em-squareascender などの値はあくまで距離の数値なので、そのまま位置算出に使うことはできなさそうです。

CSSでインライン要素の表示位置といえば vertical-align ですが、これはベースラインを揃えたりなど、わりとアバウトな配置の指示しかできませんし、 line-height の設定値次第で文字の表示位置は大きく変わります。

line-height に関して言えば、実はデフォルト値である normal は、下記の計算式で算出されているみたいです

var lineHeight = (ascender + descender + lineGap) / unitsPerEm

lineGap は、他の値と同じようにフォントのメタデータとして記録されていて、例えばArialは 67 です。フォントによっては0なこともあります。

ascender + descender が文字の表示しうる範囲ですが、lineGapはその半分の数値がascenderの上とdescenderの下に割り振られるような表示になります。

上記の式をArialのメタデータで計算してみると、結果は 1.1499 になります。約1.15です。

実際に下記のようなCSSを使ってp要素の高さをDevToolsで見てみると、115pxになってるのが確認できるはずです。

p {
  font-family: 'Arial';
  font-size: 100px;
  line-height: normal;
}

ということは、 line-height: normal を指定してJSで高さを取得し、他のメタデータからBaselineの位置を算出することで最終的にはCapHeightやxHeightなどの表示位置も計算できるはずです。

var baselineY = lineHeight - (lineGap / 2 + descender) / unitsPerEm
var xHeightY = baselineY - sxHeight / unitsPerEm
var capHeightY = baselineY - sCapHeight / unitsPerEm

結論は、CSSでフォントを深く掘るのはつらいということです