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から逃げるなということなんですかね?あぁ〜〜…


Figmaでボタン作ったらVariantsで管理するといいかんじ

Figmaにはボタンなどのコンポーネントをまとめて管理できるVariantsと呼ばれる機能があるのですが
これを実際に使ってみたところとっても便利だったので覚書として書きました。
ちなみに昨年追加された機能なのでちょっと今更感ありますが、ご参考になればと思います。

Variantsってなに?

Webデザインでよく使うアイテムは大体コンポーネント化して使っていると思いますが、
その中でもボタンやリストなど「ぱっと見同じデザインだけど色やアイコンの有無でバリエーションがあるもの」
をひとつのグループにして管理できるよ!というのがVariantsです。

やってみよう

まずはボタンのコンポーネントを作成します。
Variantsは複数管理の機能ですので、コンポーネントを2つ以上つくり、コンポーネント化しておきます。


例として赤と青のボタンを作りました。

次はコンポーネントに名前をつけます。
名前をつける際は、ボタンのデザイン規則を意識してつけます。

今回つくったのは赤いボタンと青いボタンなので、
それぞれButton / Red、Button / Blue と付けました。規則は/で区切りましょう。

名前を付け終わったら、
Variantで管理したいコンポーネントをすべて選択し、Conbine as variantsを押します。

これでVariants化できました。

ちゃんとVariants化できたか確認してみましょう。

配置したボタンを選択すると右メニューに[Property1]が追加されてます。
(この名称はVariantsのところから後で変更できます)

このプルダウンを選択すると…

ボタンを切り替えることができました!👏

これを活用すれば、たくさんのデザインをVariantsで管理できます。

ちなみにドロップダウンではなくスイッチで切り替えもできます。

スイッチ切り替えにしたい場合は名前をつける際にon/offまたはtrue/falseにしましょう。
私はアイコンのあり/なしで使ってます。

[Variants]は使えるシーンがちょっと限られる機能ですが、
便利ですのでぜひ使ってみてください。

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側で対応がなされるかもしれません。

ディスプレイを接続していない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)になってしまうので、システム環境設定から変更することをオススメします

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への移行を強くおすすめします。

ランチに行く人を勝手に選出する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人いるだけで二酸化炭素濃度はグングン上がっていきます。

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