これは株式会社POL テックカレンダー 2021 3日目の記事です。
株式会社POLでサーバサイドエンジニアをやっております岩井です。ですが、今回サーバサイドの話は一切ありません。入社から半年の間でかなり大規模なリファクタリングを自分の発案や設計でやったと思うのですが、ノウハウとして整理できている段階ではないかなと。そういうのはちゃんと整理できた段階で書きます。
前置き
自分はよくブラウザ上で動作する自分用のツールを作って色々な作業をするのですが、たまーに他の人に使ってもらってもいい出来かなと思うことがあります(大半は汎用性が全くないので公開には適していないのですが)。
旧世紀からインターネットを使っていた関係で、「インターネットに何かを公開する」=「レンタルサーバを借りてそこにアップロードする」という方式を最近まで貫いていましたが、サーバが不自由すぎたので最近AWSに移行しました。
とはいえ、AWSへのデプロイも、CI/CDでやるから気にしていないだけで、徒手空拳的にやるのは面倒だと思います。HTMLとJavascriptを公開したいだけなのに、いちいち大袈裟な設定をするのもなんか違うなと思いませんか?
規模が大きい開発についてのノウハウと比べると、極小規模のノウハウって話題になりづらい印象です。
また、自分のプログラマーとしての今のテーマは『「絵を描けるようになること」が「絵で収入を得られるようになること」を目指すことと一致しないのと同じように、「プログラミングを覚えること」が「プログラミングで収入を得られるようになること」を目指さないのが当たり前になること』を目標としているので、すごくシンプルな物を作って簡単に公開することのハードルは低ければ低いほど良いと考えています。
なので、今回は「HTMLとJavaScriptだけならgithub.ioで簡単に公開できる」というだけの話をあえてやります。
公開
公開したいものがGitHubのリポジトリとして存在している前提です。
公開したいリポジトリの Settings→Pages でGitHub Pagesの設定を開き、Sourceで公開するブランチとパスを選ぶだけで公開されます。
更新
選択したブランチにpushすると公開している物も自動的に更新されます。
これ以上簡単な公開方法が今この世にありますか??
短所
サーバサイドで処理することができないため、どうしても機能に大幅な制限がかかるのが欠点です。
自分の場合は普段こういうツールを作るときは「データはjson形式で表示するからそれをコピペしてメモ帳に保存」「画面にjsonの入力欄を設け、そこからデータをロードする」という方法で、サーバサイドに頼らずセーブ・ロードを実現していますが、あくまで個々のユーザで完結するものだからこれで済んでいるだけで、「他のユーザと相互にデータを閲覧したい」ようなものはどうにもならないですね。
JavaScriptでGitHubのAPIを叩いてブランチとプルリクを作成すれば、マージするひと手間はあるけど「サーバ側にデータを蓄えてロードする」ことは可能かな…?いつか試すかも
というわけで、HTMLとJavaScriptだけで動作する簡単なツールを超簡単に公開する話はこれでおしまいです。
明日はタイミング100%であだ名が決まった原島さんです。
作ったもの
以下、例として公開することにした、今年作ったものの説明なので読み飛ばしてもらってOKです
ツールを公開しているURL:https://waiiwai.github.io/drawTool
### 機能説明
画面左上から
#### ①プレビュー(2倍サイズ、1倍サイズ)
現代のディスプレイで1ドットを1ドットで表示するとあまりにも小さいので2倍サイズのプレビューを用意しました。まだ小さいかも。
#### ②カラーピッカー
色相環で色相を選択し、明度と彩度を選択するわりとオーソドックスなカラーピッカーです。
RGB値やHSV値の直接入力にも対応しています。
色相環をCanvasに1度ずつ描いたせいか、よく見るとモアレっぽくなっていますが気にしないでください。
#### ③パレット
16色までしか使えません。
ここで選んだ色で描画した後、パレット上の色をカラーピッカーで変更すると描画色も即時変更されます。なのでどうやっても16色しか使うことはできません。
#### ④ツール
現在は描画、消しゴム、塗りつぶしの3つしかありません。
気が向いたら全体ずらしとか、図形描画とか作るかもしれません。
#### ⑤セーブ・ロード
前述の通り、jsonでデータを出力するのでコピペでメモ帳に保存してください。
と、言いたいところですが、github.ioに公開していることによってクッキーを使えるので描画内容とパレット内容をjson形式でセーブします。
テキストボックスにも同じものを出力するので。メモ帳を利用した複数セーブデータの保持にも対応しています。
ロード時は、テキストボックスが空の場合はクッキーからテキストボックスにロードしてから、テキストボックスの内容を描画・パレットにロードします。
#### ⑥描画領域
32*32固定ですが、気が向いたら大きさを変更できるようにすると思います。
マウスなどでドラッグすることでの描画も可能ですが、アローキーとスペースでカーソルの移動と描画を行えます。
### 実装説明
あまり特筆すべき実装もありませんが、気に入ってる部分だけ。
#### 色相環の描画
const hueCanvas = document.getElementById('hueCanvas');
huecontext = hueCanvas.getContext("2d");
huecontext.lineWidth = 20;
const deg90 = Math.PI/2*3;
for (let i = -1; i < 360; i++) {
huecontext.beginPath();
huecontext.arc(100, 100, 90, deg90 + i * Math.PI/180, deg90 + (i+1) * Math.PI/180, false);
huecontext.strokeStyle = "hsl(" + i + ",100%,50%)";
huecontext.stroke();
}
円を1度ごとに色相を変えながら360度描画しています。
色相0=赤を真上にしたのですが、画像検索してみると向きも回転方向もまちまちですね。
Google画像検索:色相環
アドベントカレンダーはあくまでクリスマスのイベントなので、カラフルでそれっぽいものをお見せしております。
#### 色相環のクリック位置取得
function hueChange(x, y) {
let deg = Math.atan2(x-100, -1*(y-100))* 180 / Math.PI;
if (deg < 0) deg += 360;
selectedH = Math.floor(deg);
hueDispRefresh();
colorChange();
}
色相環に幅があり、また、ある程度内側にも外側にも余白があるので、XY座標から角度に変換して色相としています。
#### 消しゴムツールでの描画(描画の消去)
if (toolNo == "eracer" && field[y][x] != -1){
field[y][x] = -1;
dcontext.globalCompositeOperation = "xor";
dcontext.fillRect(posX+lineWidth, posY+lineWidth, cellWidth-lineWidth*2, cellWidth-lineWidth*2);
vcontext.globalCompositeOperation = "xor";
vcontext.fillRect(x*viewCellWidth, y*viewCellWidth, viewCellWidth, viewCellWidth);
v2context.globalCompositeOperation = "xor";
v2context.fillRect(x, y, 1, 1);
}
肝心なのはこれですねcontext.globalCompositeOperation = "xor";
「消す」ということを素直に考えると「透明色で描画する」イメージになりますが、それをそのまま実施すると「今塗ってある色の上に透明を塗る」という感じになり、結果的には何も起こりません。
なので、xorで重ね塗りすることで重複した部分を消しています。
#### パレット上の色が変更された時の処理
function colorChange() {
const color = "#" + colors[paletteNo];
dcontext.globalCompositeOperation = "source-over";
dcontext.fillStyle = color;
vcontext.globalCompositeOperation = "source-over";
vcontext.fillStyle = color;
v2context.globalCompositeOperation = "source-over";
v2context.fillStyle = color;
for (let y = 0; y < cells; y++) {
for (let x = 0; x < cells; x++) {
if (field[y][x] == paletteNo) {
dcontext.fillRect(cellWidth*x+lineWidth, cellWidth*y+lineWidth, cellWidth-lineWidth*2, cellWidth-lineWidth*2);
vcontext.fillRect(x*viewCellWidth, y*viewCellWidth, viewCellWidth, viewCellWidth);
v2context.fillRect(x, y, 1, 1);
}
}
}
}
これはあまり良くない実装です。
「全ピクセルをチェックして、変更があったパレットの色の場合、色を更新する」という処理になっていますが、描画領域の大きさに従ってループの計算量が増えてしまうので直したほうがいいですね。
パレット番号ごとに、塗ったピクセルを記録しておけばそのパレットで塗ったピクセル数のループにしかならないのでそうしたほうが良いはず(ファミコンなんかだとパレットの切り替えで画面全体のアニメーションを実現している(例えばドラクエの足踏みや波のアニメーションはパレット切り替えで行われている)ので、もっと効率良く全体の色を置き換える方法があるような気がしますが…)