いい感じの色を作りたいならOklchを使うといい感じになる

時折、明度と彩度を一定に保ったまま色相を変えていくつかの色を作りたいときがあります。

なすびのウェブサイトで例えると、制作事例のタグは頻繁な増減があり、数も多く、色自体に拘りたいわけではないので、すべての色をひとつずつ決めるのは結構な手間になります。
そのため、ここでは codePointAt() を使ってタグ名を数値に変換し、色相に当てはめてランダムではない動的な色を作ることにしました。

このような場合、これまではhsl()を使うことで簡単に明度彩度を固定したまま色相だけ違う色をたくさん作ることができましたが、hslによる指定では明度彩度を一定にしても視覚的な均一性はありませんでした(特に黄色系の色味が明るすぎるなど)。

最近のブラウザではhslに似た指定で視覚的均一性のあるカラー指定oklch()が使えるようになっています。

hslとoklchをわかりやすく並べて比較するとこのくらい違いがありました。

上下のどちらの色が均一に見えるでしょうか。

色相だけでなく、例えばデザインシステムのカラー設計で色相と明度(+彩度)を軸にしたカラーグリッドを作る場合でも、Oklchを使うことで視覚的に統一された色を簡単に作ることができます。

使用する上で注意点を挙げるとするならば、oklchはカラースペースがsRGBではなく、より広い範囲の色を指定することができることです。

極彩色に近い色だとディスプレイによっては色の差がわからないことがあるかもしれません。

とはいえ一般的なスマートフォンやMacbookのようなdisplay-p3対応を謳っているディスプレイであれば、ほぼ正確に出力できます。


参考にさせていただいたサイト
https://griponminds.jp/blog/relearn-css-color/

FrankenPHPでサクッとWordPressの開発環境を作る

FrankenPHPは先日v1.0がリリースされたばかりの、PHPとWebサーバがセットになったDockerイメージです。

ちょうどローカルでWordPressを動かしたいタイミングがあったので試してみました

基本的にはdunglas/frankenphp-wordpressにひな形があるのでこれを参考にします。

まずはDockerfile。
ほぼひな形通りですが、PHP8.3以上の環境だとImagickのビルドでコケるので、今回は明示的に sha-2eabec8-php8.2 を指定しています。

FROM dunglas/frankenphp:sha-2eabec8-php8.2

# install the PHP extensions we need (https://make.wordpress.org/hosting/handbook/handbook/server-environment/#php-extensions)
RUN install-php-extensions \
    bcmath \
    exif \
    gd \
    intl \
    mysqli \
    zip \
    imagick \
    opcache

COPY --from=wordpress /usr/local/etc/php/conf.d/* /usr/local/etc/php/conf.d/
COPY --from=wordpress /usr/local/bin/docker-entrypoint.sh /usr/local/bin/
COPY --from=wordpress --chown=root:root /usr/src/wordpress /app/public

VOLUME /app/public

RUN sed -i \
    -e 's/\[ "$1" = '\''php-fpm'\'' \]/\[\[ "$1" == frankenphp* \]\]/g' \
    -e 's/php-fpm/frankenphp/g' \
    -e 's#/usr/src/wordpress#/app/public#g' \
    /usr/local/bin/docker-entrypoint.sh

ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"]
CMD ["frankenphp", "run", "--config", "/etc/caddy/Caddyfile"]

docker-compose.ymlはそのままでも大丈夫そうだけど、好みの問題で下記のようにしました

version: '3.1'

services:

  wordpress:
    build: .
    ports:
      - 80:80
      - 443:443
    environment:
      WORDPRESS_DB_HOST: db
      WORDPRESS_DB_USER: docker
      WORDPRESS_DB_PASSWORD: docker
      WORDPRESS_DB_NAME: dbname
    volumes:
      - ./:/app/public

  db:
    image: mysql
    environment:
      MYSQL_DATABASE: dbname
      MYSQL_USER: docker
      MYSQL_PASSWORD: docker
      MYSQL_RANDOM_ROOT_PASSWORD: '1'
    ports:
      - 3306:3306

上記2ファイルをドキュメントルートの階層に置いて、起動すればlocalhostでアクセスできました。

docker compose up

aspidaのRestClientで多重送信を抑制する

OpenAPIでAPIが定義されているプロジェクトにおいて @aspida/fetch を使ってRestClientを生成・利用しています。

とあるGETリクエストを複数のコンポーネントからほぼ同時に呼び出す必要があり、リクエストの回数を1回にまとめたい状況がありました。
これをaspidaでやろうと思ったときにちょっと思ったようにできなかったのでメモ。

最初にやろうとした実装は、aspidaに渡すfetchをラップする方法。

const promises = {};
function customFetch(url, option) {
if (option.method === 'GET') {
const key = `GET__${url}`;
if (!promises[key]) {
promises[key] = fetch(url, option);
}
return promises[key];
} else {
return fetch(url, option);
}
}

promiseを再利用したいわけですが、こうすると、ResponseのStreamが使用済みなのでエラーになります。そりゃそうだ。

Response.clone() で複製してから使えばよいわけですが、aspidaの生成するtsコードで Response.json() を実行してしまうので、外からcloneを挟むようなことは無理そう…

色々考えた結果、こうなりました

const promises = {};
function dedupe(api, option) {
const key = `GET__${api.$path(option)}`;
if (!promises[key]) {
promises[key] = api.$get(option);
}
return promises[key];
}

使う側では

const result = await client.path.to.hoge.$get({ query: { fuga: 'piyo' } });

これを、

const result = await dedupe(client.path.to.hoge, { query: { fuga: 'piyo' } });

こうする。

関数で囲わなければいけなくなったのがちょっとスマートじゃないですが、まあ重複排除したいところだけ使えてポータブルな関数になったので良いということにしよう。

Google Publisher Tag のよくあるエラー

googletag.openConsole() で表示される警告文の意味と主な対処方法の自分メモ

用語

GPT: Google Publisher Tag
GAM: Google Ad Manager

ドキュメント

⚠️ 広告ユニットが取得されませんでした

何が起きているか

defineSlotは行われたが、displayやrefreshを行っておらず、広告リクエストが走っていない状態

対処方法

displayやrefreshを実行する。

googletag.display('div-id');
googletag.pubads().refresh([slot]);

disableInitialLoad() が利用されている場合は、displayでは広告リクエストが走らなくなるのでrefreshを使う。

⚠️ 広告ユニットが埋められませんでした

何が起きているか

配信条件に一致する広告申込情報が見つからずに広告を埋められなかった

対処方法

GAMで該当の広告ユニットと広告申込情報の配信設定やターゲティングを見直す

CSSのfont-synthesisとtext-renderingが原因でAndroidで太字にならない

Viteで新規にプロジェクトを作るとapp.cssに font-synthesistext-rendering が指定されています。

:root {
  ...
  font-synthesis: none;
  text-rendering: optimizeLegibility;
  ...
}

これらのプロパティがあると、 Android Chrome で見たときにfont-weightで700以上を指定しても太字にならない場合があります。

font-synthesis は、フォントに太字やイタリックの字体が含まれていない場合にブラウザが合成してよいかどうかを制御するもの。

text-rendering は、合字やカーニングの処理を行うか否かを制御し、読みやすさと速度のどちらを優先するか決めることができます。

これら2つの両方もしくはどちらかと、おそらく font-family によるフォント指定の組み合わせによって太字書体が使える環境にも関わらず太字にならないことがあるようです。

基本的に欧文フォントのためのプロパティなので、日本語環境では、少なくとも現段階では使わないほうが無難なようです。

Web Designing 2022年12月号にインタビューが掲載されました

2022年10月18日にマイナビ出版さんより発売された「Web Designing 2022年12月号」に菅沼のインタビューを掲載していただきました。

12月号はJavaScriptフレームワーク特集ですが、その中の「モダンフロントエンド開発技術の使い分けと現場の実情」というコーナーで、「普段どういう目線でフレームワークを選定しているのか?」「VueやReactなどを使う際に気をつけるべきこと」など、鼎談形式で話をさせていただきました
技術の深い話ではなく、フレームワークを使うと何が変わるのか、といった内容が主なので、現役のエンジニアよりはディレクターに向けた内容になっています

出版社サイトはこちら:
https://book.mynavi.jp/ec/products/detail/id=132319

ご興味があればぜひご覧ください!

クリエイティブに最適なAG274UXP/11をレビュー

以前の記事で、制作現場で使える最強ディスプレイを探した結果 Eve Spectrum を購入したことをお伝えしたが、今回2台目としてAOCのAG274UXP/11を購入した。
Macbook のディスプレイや Eve Spectrum とも比較をしながらレビューしていく

今回購入したAG274UXP/11はAOCというメーカーのもので、日本ではあまり聞き馴染みがないが欧米ではわりとメジャーな、台湾の老舗ディスプレイメーカーらしい。
実際にドット抜けもなく非常にコスパに優れた良い製品でした。

スペック

制作現場で重要となる項目について、AG274UXP/11のスペックは下記の通り

解像度:4K
ビット深度:True10bit
リフレッシュレート:160Hz
色域:P3 102%

2021年の M1 Pro/Max を搭載した MacbookPro が ProMotion を採用したので、外部ディスプレイで必ずしも120Hzが必要かと言われるとそうでも無くなってきているのだが、特筆すべきはP3の100%を超える色域だろう。
MacbookのP3カバー率は明記されていないが、実測で96%程度らしいので、おそらく公称値98%のディスプレイと同程度と考えると良さそう。
少なくとも AG274UXP/11 は、 Spectrum や MacbookPro より広い色域を表現できるということだ。

色域以外でもビット深度が8bit+FRCではなく True 10bit であることやリフレッシュレートが最大160Hzである点など、完全な上位互換となっている

あと、おまけでモニターフードもついてきます
マルチディスプレイとは相性が悪いが、つけてるとデキるデザイナーっぽい演出ができるのでオススメ

実際に繋いでみた

Spectrum では、おそらくディスプレイのファームウェアの問題で type-c to type-c で繋ぐと60Hzまでしか出力できず、 type-c to DP で繋いで4K144Hzを出力していたが、AG274UXP/11では type-c to type-c で繋いでも4K120Hzを選ぶことができた。
144Hzや160Hzが出力できない理由は現状わかっていないが、120Hzあれば制作用途であれば必要十分なので問題ない。

もちろんThunderboltなので、1本繋ぐだけでディスプレイをUSBハブとしても利用可能。
電源供給は60Wなので M1 Pro/Max のマシンでは少し心許ないが、高負荷な作業を連続して行わない限りバッテリーが目減りすることはないので個人的には不満には感じていない。

ロゴプロジェクターと背景のライティングは不要

やはりゲーマー向けモデルなだけあって背景は虹色に光る。
それだけならよくあるゲーミングディスプレイなのだが、AG274UXP/11に至ってはなんとロゴプロジェクターまでついている。

AOCの、このフラッグシップモデルに対する熱い思いはひしひしと伝わってくるが、まったくもって不要である。

ちなみに、背面のライティングとロゴプロジェクターはRGBそれぞれ100段階で色の変更も可能なので、最初の10分くらいは楽しく遊べます。

設定からすべてオフにできるので不要な方もご心配なく。

現状最強ディスプレイ

制作現場で使うディスプレイとしては完璧と言っていいくらいのスペックで、それに対する価格では最強と言っても過言ではないと思う

もし120Hz環境が必要ないのであれば、 HUAWEI の MateView もMacbookレベルの色域と広い作業領域が7万円程度で手に入るので非常におすすめ。
ちょっとクセは強めですが。

  • 解像度が4Kよりも縦に広い(デザインツールなどでは使いやすいと思う)
  • VESAマウントに非対応

AG274UXP/11はVESAにも対応してるけど、アームでロゴプロジェクターって相性悪くないのかなあ。

JSで(-8)**(2/3)を計算するとNaNになる

タイトルの通りなのですが、なぜかとある条件のべき乗を計算するとNaNが返ってくるので不思議に思って調べてみました(調べてくれたのは神谷)。

その条件は、基数が負なおかつ指数が分数 であること。

たとえば (-8)**(2/3) という計算は、答えは 4 なのですが、JSで計算をするとNaNが返ります。

(-8)**(2/3) // NaN

一体全体何が起きているのか?
答えは MdNのMath.powのページ に書いてありました。

// due to "even" and "odd" roots laying close to each other,
// and limits in the floating number precision,
// negative bases with fractional exponents always return NaN

最初の一行の意味はわからなかったのですが、要するに浮動小数点の精度の問題で 基数が負なおかつ指数が分数のときは静的にNaNを返すのがJSの仕様 ということみたいです

実際にMath.powで同じことをしても結果はNaNでした。

Math.pow(-8, 2/3) // NaN

試しに、指数の分数を分解して (3√(-8))^2 で計算してみると

Math.pow(Math.cbrt(-8), 2) // 4

正しく4が返ります。


そういえば、正の数の平方根の計算結果って±になりますよね。

√4 の答えは ±2 と学校で習った記憶がある。

Math.sqrt(4) // 2

この辺の計算って結構曖昧なのかもしれません

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でリリースされるみたいなのでもう少し待ってみるか