ウェブサイトでSVGを配置して、cssでドロップシャドウを付ける話

以前の投稿では、SVGの色をcssを使って変更する方法についてお話ししました。今回は、SVGに影(ドロップシャドウ)を追加する方法に焦点を当てたいと思います。

divにドロップシャドウを付ける場合は、以下のようにcssのfilterプロパティでdrop-shadow関数を使用することができます。

.divShape {
  display: block;
  width: 500px;
  height: 500px;
  background: #222;
  filter: drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4)); 
}

前回のカスタムシェイプの場合、この方法ではドロップシャドウが適用できません。

この場合、擬似要素を使用します。::before擬似要素をカスタムシェイプに適用し、その親であるdivにドロップシャドウを追加します。

このようにすることで、カスタムシェイプのdivにドロップシャドウを効果的に付けることができます。

.svgShape {
  position: relative;
  width: 606px;
  height: 514px;
  filter: drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4)); 
}

.svgShape::before {
  display: block;
  height: 100%;
  content: "";
  background: linear-gradient(45deg, black, transparent);
  mask-size: contain;
  mask-image: url('/img/sample.svg');   
}

ウェブサイトでSVGを配置して、cssで色を変更する話

SVGをウェブページに追加する最も簡単な方法は、<svg>タグをインラインで直接記述するか、<img>要素を追加してsrc属性でSVG画像を参照することです。
SVGファイル直接に使用すると、SVGの色を変えたい時は少しややこしいです。(編集ソフトを使用して、書き直す必要があります。)
(<img> 要素を使用してインポートした場合、CSSだけで色を変更することはできません。)

単色を変えたい場合:
css実装例:

#svgID path{   
  fill: red; 
}

このようにすればcssでpath要素の色を簡単に変更できるだと思いますが、グラデーションをかけたい場合はSVGファイル自体を修正しなければなりません。そこでもう一つの方法を紹介します。

DIV要素をカスタムシェイプに:

SVGインラインで直接記述するより、DIV要素の形をSVGにすれば、色々カスタマイズができます。今回は外部SVGを参照して、cssで色をグラデーションにします。

divといえば、多くの人のイメージには長方形や正方形しかありません。しかしdivを他の形にすることができます。この時はmask-imageを使います。
(単純にdivの形を変えたい場合はclip-pathも出来ますが、今回の主旨と違いますので詳細は割愛させていただきます)

HTML実装例:
<div class=”svgShape”></div>
css実装例:
(widthとheightはsvgのサイズに合わせてください)

.svgShape {
  width: 500px; 
  height: 500px;
  background: linear-gradient(45deg, black, transparent);
  mask-image: url('/img/sample.svg');
}


このように実装すれば、divはsvgの形に変えられ、色はcssでカスタマイズできます。
実装例:

refrence:

https://developer.mozilla.org/ja/docs/Web/CSS/mask-image

ColimaのDocker内で `composer install` に失敗する問題の解決

M1やM2などのArm Macで、かつ Colima でDockerを使っている場合、Docker環境内で composer install でするとコケる事がある。
厄介なのは、コケたりコケなかったりColimaごと固まったり非常に不安定なことがある。
その場合恐らく、デフォルトの qemu 環境で立ち上がっていると思われるので、一旦VM環境を削除した後に

colima start --vm-type vz --vz-rosetta --mount-type virtiofs

のようにVZ環境で作り直すとうまくいく。

Virtualization.framework(vz)はMac OS 13から利用できる仮想環境のAPIで、I/Oも早くなるらしい。

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' } });

こうする。

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

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 によるフォント指定の組み合わせによって太字書体が使える環境にも関わらず太字にならないことがあるようです。

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

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

sass-loaderとdart-sassにまつわるfibersの話

tl;dr

用語について

現在npmで sass として公開されているパッケージはdart製のsassとなっています。旧来使われていたnode製のsassは node-sass として公開されています。

ここでは区別を分かりやすくするため、dart製のsassを dart-sass、 node製のsassを node-sass と呼び分けています。

本文

そもそもsass-loaderで何を設定しているのでしょうか。sass-loaderでdart-sassを利用する際はだいたい下記のように設定しているかと思います。

// webpack.config.js
module.exports = {
  // ...
  module: {
    rules: [
      {
        // ...
        use: [
          `// ...
          {
            loader: `sass-loader`,
            options: {
              implementation: require('sass'), // ここで `dart-sass` を読み込んでいる
              sassOptions: {
                fiber: require(`fibers`), // 大体の人がなんとなく一緒に読み込んでいる
              },
            },
          },
        ],
      },
    ],
  },
  // ...
};

まず implementation プロパティについてはsass-loaderのドキュメント に書いてあるとおりですが簡単にまとめると

  • implementation を設定しない場合、 node-sassとdart-sassの両方インストールされている場合、自動的に後者が読み込まれる
  • implementation を設定することで明示することができる

の2点かとおもいます。この「両方がインストールされている場合」というのは、プロジェクト内の package.jsondevDependencies などに追加されている場合以外にも他のパッケージの依存に入っている場合も含まれるので、基本的には明示したほうが良いでしょう。

では、なぜ dart-sass を利用する場合に fiber プロパティにfibersパッケージを指定しているのでしょうか。

node-sassにもdart-sassにも render()renderSync() というAPIがあります。これはどちらもscssからcssへコンパイルするための関数ですが、前者が非同期に実行され、後者は同期的に実行されます。sass-loaderでは render() が利用され非同期的にscssファイルがコンパイルされることになります。

しかし、 dart-sassでは非同期にに実行する render() が非同期実行のオーバーヘッドのため renderSync() より2倍近く遅くなってしまいます。これを避けるために、利用されるのがfibresです。

fibersはもともとnode.jsに非同期に関数を実行する仕組み(Promiseやasnyc/await)がなかった頃に開発されたものです。同期関数を非同期で実行することができる Promise に相当する機能があります。(他にもGeneratorに相当する機能もありますがここでは割愛します。)

速度の早い renderSync() 関数を非同期でするために、このfibersを利用する設定が前述した webpack.config.js です。ただしfibersは作者により使用を避けるべきとされており、node 16への対応もされていません。

まとめ

つまり記事の冒頭でまとめたように、 node 16 を利用するのであればfibersを利用することはできず、コンパイル速度の低下を受け入れざるを得ません。sass-loaderではdart-sassの場合に renderSync() を利用するIssueが立っているのでもしかするとsass-loader側で対応がなされるかもしれません。

nvmとかnodenvとかanyenvをやめてasdfにした

これまでnode環境はnvmで、他はpyenvやgoenvを利用していたのですが、nvmが .node-version ファイルに対応していませんでした。(.nvmrc で指定することはできませす。)
そこでいい加減nodenvに移行したのですが、せっかくなのでどうせなら新しい物をを使おうと思いasdfに移行しました。

anyenvの問題点ははただの「**envをインストールソフト」であり、nodenvやpyenvなどそれぞれの利用法を知っている必要があります。
asdfは実行環境ごとのプラグインを介して直接バージョンを管理するため
どの実行環境でも同様に扱うことができます。
ある日突然「PHPの5.3のバイナリが欲しい…」となっても使い方を迷う必要がありません。

例としてnodejsであれば

asdf install nodejs 16.1.0

Pythonであれば

asdf install python 3.9.5

でインストールが可能です。

また、nodejsであれば lts-fermium のようにタグで指定することも可能です。
自分はグローバルにインストールするものは「一番新しいLTSの最新バージョン」を利用することにしているので、非常に便利です。(nodenvではこれができませんでした。)
また、.asdfrclegacy_version_file = yes を追加することで .node-version なども自動的に読み込み、ディレクトリによって自動的にバージョンを切り替えることもできます。

注意点としては、ビルドする関係上、使用するプラグイン(実行環境)によって依存するライブラリがあるため、そのインストールは別途必要になります。

anyenvより圧倒的に使いやすいので、anyenvやその他のバージョン管理ツールを利用している場合は、asdfへの移行を強くおすすめします。