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なんて使ってんのが悪い。

Photoshopを使って3DCGつくってみよう

ここ数年3Dの作品をよく見かけるようになり、
やってみたくても中々ハードルが高くて躊躇していたのですが…
普段使ってるPhotoshopで3Dつくれるらしいぞ → 実際にやってみた!ので記事にしてみました。

用意するもの

  • Adobe Illustrator(作画)
  • Adobe Photoshop(3D作画)
  • Adobe Dimension(3D出力)

CreativeCroud課金者なら上記ぜんぶインストールできます
(※ちなみにPhotoshopでパス書く場合はIllustrator不要です)

主な流れとしては、
① Illustratorでパス書く
② Photoshopで3Dにする
③ Dimensionで3Dを編集する

の3工程です。

早速やっていこう

今回は私のキャラクター「おもち」を3Dにしたいと思います。

おもちです。白くてまるい。足が4本、角が3本あります。

1. パスデータをつくろう


Illustratorを起動してペンツールでキャラクターのシルエットのパスを書きます。
(※ Photoshopでパス描ける方はIllustrator使わなくてOKです)

パスを描くときは3Dにした際のガタつきを減らすため、
なるべくアンカーポイントを減らし、左右対称にするとよい感じです。
また、丸いシルエットにするためコーナーを角丸にしておきます。
(色は後で変更するのでわかりやすいよう水色にしています)

保存形式はaiでOK。体と顔パーツは別のaiファイルにしておきます。

2. Photoshopで読み込んで3Dにする

先ほど作成したaiファイルをPhotoshopで読み込みます。
解像度は300dpi、カラーモードはRGBカラーにしましょう。
(RGBカラー以外だと3D作成できません)


読み込み後はレイヤーを選択し、
メニューバーの[3D]→[選択したレイヤーから新規3D押し出しを作成]を選択します。
図がなんとなく3Dぽくなって、3Dメニューが出ます。


3Dメニューのレイヤー1を選択し、プロパティのシェイププリセットからピロー効果を選択します。すると何だか表面がぷっくりします。


次はプロパティのキャップに移動し
変数を[フロントとバック]、膨張の角度を[90°]強さは[20%]に設定します。
強さを上げるとふくらみが強くなります。


好みのふくらみ加減になったら、座標→座標を初期化をクリックして座標位置をリセットしておきます。


ここまでできたら書き出しましょう。
メニューバーの[3D]→[3Dレイヤーを書き出し]を選択します。
3Dファイル形式を[Wavefront|OBJ]に変更して保存します。


同じ工程で顔パーツも作成します。
すべてのパーツができたら、次はDimentionで3Dデータを編集します。

3. Dimentionで3Dを編集する

Adobe Dimensionを起動して、ファイル→開くで先ほど作成した体パーツの.objファイルを読み込みます。
その後、顔パーツの.objファイルを上にドラッグしてパーツが2つ並ぶように配置します。

ここから軌道カメラツールなどを駆使しつつ顔パーツを体パーツに埋め込みます。


↑軌道カメラツール。ぐるぐる回る

↑パンツール。近寄ったり離れたりする

遠近ツール。手前に寄せたり遠ざけたりする

カメラツールをいじりすぎて位置がごちゃごちゃになってしまったら
メニューバーのカメラ→ホームビューに切り替え(または⌘B)で初期位置に戻しつつがんばります。


顔パーツを選択し、緑色の矢印を選択して十字キー↑を連打。顔の位置を少しずつ上げます。
位置が定まったら次は赤色の矢印を選択して少しずつ横に移動させます。


カメラの視点を上に持っていき、顔パーツが半分埋め込むまれるよう調節します。
納得のいく位置に調整できたら、最後に顔パーツと体パーツをグループ化しておきます。

3Dの組み立てが完成したら後は質感と色を変えます。

マットな質感にしたいので、本体パーツをクリックして
スターターアセットの中にある[Adobe標準マテリアル]から[艶消し]を選びます。
プロパティのベースカラーを白色(RGB 255,255,255)に変更します。
顔パーツも艶消しの黒色(RGB 20,20,20)にしました。


ここまでできたら、あとは背景を合成してみます。

4. 背景を敷いてみる

右上のシーンメニュー内にある[環境]を選択すると、
環境プロパティがいじれるようになるので、ここで好きな背景画像をおきます。

背景を置いた後、アクションから
☑️ カンバスサイズを変更→画像の縦横比
☑️ ライトを作成→抽象
☑️ カメラのパースを合わせる にチェックを入れOKボタンを押します。


すると勝手に位置やライティングを調整して、背景に合うよういい感じに仕上げてくれます。すごい。
あとはお好みで位置や大きさなどを微調整しましょう。

完成したら、デザインメニュー横のレンダリングを選択し、
画質や書き出し形式を好きなものに設定して書き出します。

完成!!


レンダリングしたものがこれになります。
路地裏で猫ちゃんに遭遇したみたいでかわいい~!!

あとがき

Dimensionは今回初めて触ったのですが思ったより直感的に触れてなんとかなりました。
パッケージデザインのモックアップを作るのに適しているそうなので
次回はそっちでも遊んでみたいと思います。

そしてこの記事を書いている間に気がついたのですが、
実は近日中にPhotoshopから3D機能が削除されるみたいです。えっ…??

Photoshop の 3D 機能 | 廃止された 3D 機能に関するよくある質問

Photoshop22.2を使用すれば引き続き3Dを使えるみたいなんですが、
まさかこんなタイミングで削除のお知らせを見るとは…

やっぱりBlenderから逃げるなということなんですかね?あぁ〜〜…