2020年3月20日金曜日

getUserMedia() のアスペクト比を保つ方法

JavaScript でカメラを利用できる getUserMedia() を、様々な環境でビデオサイズのアスペクト比を保つのは、意外とコツがいる。 少しハマったので回避方法をメモしておきます。

まずビデオの解像度を変更するための基本的な関数は以下になります。 カメラを止めて設定し直すような処理を入れておくのがポイント。
let video = document.createElement("video");

function syncVideo(video, options) {
  if (video.srcObject) {
    video.srcObject.getVideoTracks().forEach(function(camera) { camera.stop(); });
  }
  navigator.mediaDevices.getUserMedia(options).then(function(stream) {
    video.srcObject = stream;
    video.setAttribute("playsinline", true); // required to tell iOS safari we don't want fullscreen
    video.play();
    requestAnimationFrame(tick);
  }).catch(function(err) {
    alert(err.message);
  });
}

このコードを利用して、PC の Chrome などで「可能な限り w, h の大きさを維持して」アスペクトを保ったビデオを起動することを考えます。 カメラの解像度が対応していなかった時にアスペクト比が崩れないようにしないといけません。 aspectRatio を exact で最優先にすれば良さそう、と以下のオプションを考えるかと思います。
const options = { video: { facingMode: "environment",
  width:w, height:h, aspectRatio:{ exact:w/h } } };
syncVideo(video, options);

しかしこのコード iOS が動かないんですよねえ。 よって、様々な解像度に対応するビデオ呼び出しは以下のように書く必要があります。
const options = { video: { facingMode: "environment",
  width:{ min:0, max:w }, height: { min:0, max:w }, aspectRatio:w/h } };
syncVideo(video, options);


わかれば単純だけど、ここに辿り着くのに凄い時間が掛かりました。

2020年3月18日水曜日

JavaScript で Clipboard の現状まとめ

JavaScript で Clipboard を操作しようとすると、本当によくハマる。 セキュリティ意識が高まってきた関係で API が山のようにあってコロコロ変わる。 ようやく多少の方向性が見えた気がするので、情勢をメモしておきます。

1. Flash経由でクリップボード操作

JavaScript にクリップボード操作の API が何もなかった時代には、Flash でクリップボードを操作するという技が使われていました。 しかし Flash が完全に死滅しつつある現在、この技は完全に使えないものになりました。

2. execCommand

その代わりに出てきたのが、execCommand です。 しかし昨今のセキュリティ事情の変化により、動かない利用用途も出てきています。 また「選択範囲を」「現在の場所に」などの制約があるので複雑なことには使えません。
  • document.execCommand("copy"): 選択範囲をクリップボードにコピー
  • document.execCommand("cut"): 選択範囲をクリップボードにカット
  • document.execCommand("paste"): クリップボードを現在の場所にペースト

3. copy/cut/paste Event

ユーザのイベントを補足して処理することもできます。 ここが一番の落とし穴で、サポート状況見ると動きそうだけど、これ昨今のセキュリティ事情の変化を受けてか、contentEditable なフィールド以外だと、Chrome が全然動かないんですよ。 一番わかりやすい例が これ ですが、動かない。 Firefox ではまだ動いたり流動的な情勢で、あまり深いことは言及を避けたほうが良いけど、いずれ動かなくなると予想します。
  • document.addEventListener("copy")
  • document.addEventListener("cut")
  • document.addEventListener("paste")

4. navigator.clipboard

ではどうするのかというと、navigator.clipboard を使います。 ユーザにクリップボードの利用許可を得てから、クリップボードを操作する API です。 いつできたは正確にはわからないのですが、Chrome 66 くらいからに見える。 async なイベントなのでこれまでの実装とは大きく異なる上、まだ実装も揺れ動いているみたいで Chrome 80 で実装方法が変わりました。

read, readText, write, writeText の 4 つのメソッドがあり、readText と writeText は安定してきているみたい。 問題は read, write のほうで、テキスト以外のクリップボード操作もできて強力なのですが、まだブラウザ実装も少なく、揺れ動いています。 Chrome の古い実装例ならいくつかまとまっているのですが、新しい実装方法はまだ解説記事も少なく、この記事くらいしか見当たらなかったので、自分なりの実装をメモしておきます。

以下はクリップボードに画像があるとき、その画像を canvas に描くサンプルです。 ちなみに Chrome 80 以上でないと動きませんし、執筆時点では Firefox も Safari も動きません。 今のところ Firefox, Safari は copy/cut/paste Event で実装したり、諦めるなどの回避策を取る必要があります。
document.getElementById('pasteButton').addEventListener('click', function() {
  navigator.clipboard.read().then(function(data) {
    for (let i=0; i<data.length; i++) {
      const img = data[i];
      console.log(img);
      for (const type of img.types) {
        if (type.indexOf('image') != -1) {
          img.getType(type).then(function(blob) {
            const img = new Image();
            img.onload = function() {
              const uploadCanvas = document.getElementById('uploadCanvas');
              uploadCanvas.width = img.width;
              uploadCanvas.height = img.height;
              uploadCanvas.getContext("2d").drawImage(img, 0, 0, uploadCanvas.width, uploadCanvas.height);
            }
            img.src = URL.createObjectURL(blob);
          });
        }
      }
    }
  });
});



navigator.clipboard は強力で良さげな API に思います。 とは言えセキュリティ意識の高まりを受けて、ブラウザの実装がまた混沌としてきた気がします。 しばらく実装が面倒そう。

2020年3月15日日曜日

Web components の使い方とポイント

Web components を使ってアプリを作って、思うところがあったのでまとめておきます。

1. Web components はみんなが学ぶ必要がありそう

最初に結論から言うと、Web components はフロントエンドを少しでもいじってる人は、みんな学ぶ必要がありそうと思いました。 私も最初は使いたい人だけ学べば良いかと思っていたけど、少しでも Web components を使ったテンプレートやライブラリを操ろうとすると、仕組みを知っていないときちんと扱えない。

JavaScript は ES5 時代は minimal でとても良かったんですが、最近はかなり広く知る必要が出てきています。 しかし Web components 回りのドキュメントは現状読みにくいものばかりだったので、自分用備忘録として重要なところだけまとめました。 ちなみに今のとこ一番 Web components の説明がわかりやすいかなと思ったのは以下です。 この記事はその補足みたいなもんです。

2. template の制約に注意

最近作っていたアプリで作った img-box という Shadow DOM を例に挙げます。 img-box は画像に閉じるボタン (svg のところ) を付けるだけの、シンプルな Shadow DOM です。
<template id="img-box">
  <style>
    .thumbnail { position:relative; float:left; padding:5px; }
    .toolbar { position:absolute; top:0; left:0; padding:10px; }
    svg { cursor: pointer; }
  </style>
  <div class="thumbnail">
    <img src="dummy.jpg" width="150" height="150" alt="thumbnail">
    <div class="toolbar">
      <svg class="close"> ... </svg>
    </div>
  </div>
</template>
<script>
customElements.define('img-box',
  class extends HTMLElement {
    constructor() {
      super();
      const template = document.getElementById('img-box')
        .content.cloneNode(true);
      template.querySelector('.close').onclick = function() {
        this.parentNode.parentNode.remove();
      };
      this.attachShadow({mode: 'open'}).appendChild(template);
    }
})
</script>
attachShadow などのお決まりみたいな部分は説明しませんが、Shadow DOM を定義するに当たっては 2 つ重要なポイントがあります。
  1. template 内の探索には querySelector / querySelectorAll しか使えない
  2. template 内のイベント定義は制約が多いのでコンストラクタで実装すべき
1 は、getElementById のような DOM 操作コマンドが使えないということです。 DOM 操作では速度を意識して get 系を使いたくなるので注意が必要。 たぶん通常のエレメントと処理を変えることで軽量化・高速化しているんでしょうねえ。

2 は JavaScript Framework に慣れた人はこれを Web components の不満としてよく挙げます。 ただコンストラクタで実装すれば何の問題もありません。 コンストラクタ以外だと、connectedCallback() に実装するのが良いかと。

どちらも、そういうものなのだと思えば、難しいところはないです。


3. Shadow DOM の呼び出しには独自メソッドを用いる

多くの人にはこちらが重要で、Shadow DOM の呼び出しには独自メソッドを用いる必要があります。 例として、Shadow DOM を呼び出して一部分だけ要素を書き換えてみます。
function snapToThumbnail() {
  affineImage();
  var thumbnail = document.createElement('img-box');  // img-box を生成して
  var img = thumbnail.shadowRoot.querySelector('img');  // 中の img タグを探して
  img.src = canvasCV.toDataURL('image/jpg');  // src を書き換える
  outputElement.append(thumbnail);  // 最後に body に img-box を出力する
}
このコードで重要なポイントは 2 つあります。
  1. Shadow DOM 内の探索には shadowRoot オブジェクト配下を見る必要がある
  2. Shadow DOM 内の探索には querySelector / querySelectorAll しか使えない
特に 1 が重要ですね。 「shadowRoot を通じてアクセスする」を知ってないと、探したい要素が見つからないで苦労するかと思います。 これがみんなが知っているべき理由。 iframe の処理でも独自のオブジェクト配下を見る必要がありますが、それに似ています。

Web components はイベント回りに踏み込まないように心掛ければ、ハマりそうなポイントはこれくらいです。 使うことが当たり前になれば結構使いやすい機能と思う。

2020年3月1日日曜日

フリゲ紹介: Bubble Woods

GameSnacks という Google 製のフリゲ投稿サイトができました。 まだできたばかりなので 6 作品しかないですが、パブリッシャーを募集しているようで、今後ゲームが増えていきそうです。 私は フリーゲームを探すのに便利なサイトまとめ というまとめページを作っていますが、ここ 2-3年くらいでフリゲのサイトがどんどん増えてきていることを感じます。 この感じだと、世界的にもフリゲは流行するのではないかなあ。


とは言え、海外のフリゲは超ライトなものがほとんどで、日本のフリゲ感とは少し違います。 特に HTML5 ゲームだと、差を感じて、世界のフリゲサイトと比較しても、日本のフリゲ界のクオリティは突出して高いです。 世界の人が日本のフリゲをもっと見てくれれば良いなあと思いますが、日本から世界に出しても十二分に通用すると思います。

GameSnacks が出たこともあって、様々な海外のフリゲサイトで、HTML5 ゲームでランキング上位のゲームを何十個かパパっと触ってみました。 私が一番面白いなと思ったのは、GameSnacks で最初に公開された 6 つのゲームのうちの 1 つ、Bubble Woods でした。 まずはその紹介から。



画面下の大砲から、4色のボールを発射し、同じ色が 3 つ以上くっついたら、ボールが消えるという、わかりやすいゲームです。 シンプルさが良いですね。

ただボールを消すだけでなく、ボール同士の連結をうまく切り離すと、一気に高得点が狙える仕組みもあります。 例えばこの画像では、真ん中らへんの青いボール群に大砲から青玉を当てれば、その下にあるボールがすべて地面に落ちて、高得点が狙えます。 シンプルながらひねりが効いていて、面白いパズルゲームと思います。

これ以上のクオリティを出せれば、海外でもだいたいヒットするのではないかと思います。 フリゲ製作者の方は、海外も視野に入れても良いのかも知れません。