ナンモワカランアザラシ

技術的なアレコレを自分の言葉で書いて保管・公開しておくための静かなインターネット

モニターの解像度に合わせてCanvas要素のサイズを変える

モニターによって、1単位長さに表示できるピクセル数は異なる。 Canvas要素のstyle上の大きさと描画の大きさを気にしてあげないと、モニターによってはぼやけてしまう。

それを回避するための基本的な考え方を記載する。

window.devicePixelRatio

https://developer.mozilla.org/ja/docs/Web/API/Window/devicePixelRatio

devicePixelRatio は Window インターフェイスのプロパティで、現在のディスプレイ機器における CSS 解像度と物理解像度の比を返します。

この値を使う。

Canvasのresize

Canvasのstyle上の大きさが所与であるとする。styleWidth, styleHeightとする。

// canvas要素の描画サイズを適応させる
const resizeCanvas = (canvas: HTMLCanvasElement) => {
  const dpr = window.devicePixelRatio || 1
  canvas.width = styleWidth * dpr
  canvas.height = styleHeight * dpr
}

以上が基本的なコードである。

CSS上のサイズがレスポンシブだったり、縦横比が変化したり、CSS上の縦横比とcanvasの縦横比が異なったり、クリック地点によって処理を変えたかったり、CSS上のサイズ・Canvasの描画サイズ以外にアプリケーションで管理するスクリーン概念のサイズがあったりすると考えることが多くなって疲れる。

レスポンシブ

ResizeObserverを使う。スタイル上の大きさが変わるたびに描画サイズも変えるのだ。 https://developer.mozilla.org/ja/docs/Web/API/ResizeObserver

クリック位置

直接的に「Canvasのスタイルサイズにおけるクリック位置」を取得できる方法はないので、画面上でのクリック位置とCanvasの位置で差分をとる。

const canvas = document.createElement('canvas')
const dpr = window.devicePixelRatio || 1
const handleClick = (e: MouseEvent) => {
  const canvasBoundingClientRect = canvas.getBoundingClientRect()
  
  const canvasStyle = window.getComputedStyle(canvas)
  const borderWidthLeft = parseFloat(canvasStyle.borderLeftWidth)
  const paddingLeft = parseFloat(canvasStyle.borderTopWidth)
  const canvasPositionLeft = canvasBoundingClientRect.left + borderWidthLeft + paddingLeft
  const clickX = (event.clientX - canvasPositionLeft) * dpr
  doSomethingWithClickPositionInCanvas(clickX)
}

ややこしいのでCanvasにborderとかつけないほうがいい。

縦横比

変化するたびにgetComputedStyleなどでstyleサイズを取得してresizeをし直す。先のコード例の通り、取得方法によってborderやpaddingの値を含んだり含まなかったりするから注意。 CSSサイズとCanvasサイズで縦横比が異なる場合はそんなに考えることは多くない。ただ描画が歪むだけだ。分かりやすい。

CSSサイズでもCanvasサイズでもない「サイズ」

例えばゲームのサイズとして横幅を"gameWidth"と定める。キャラのサイズとの比の兼ね合いなどあるので、大抵の場合この値は固定値だ。 Canvas要素のスタイルサイズを画面いっぱいにしたかったら、縦横どちらを余らせてもいいかを決めてから、ゲームサイズの比と合うようにする。 解像度に合わせたいのでCanvasの描画サイズはdpr倍される。 さて、Canvasの描画サイズに対してゲームサイズの1単位はいくつだろう?クリック位置をゲームのサイズで測りたい場合は?

Canvasのスタイル上でのクリック位置をclickXとする。 Canvasのスタイル上での横幅をStyleWidthとする。 Canvasの描画サイズはStyleWidth * dprだ。 クリック位置のゲーム中での位置は、clickX * dpr * gameWidth / (StyleWidth*dpr) = clickX * (gameWidth/StyleWidth)となる。ゲームサイズとスタイルサイズの比をかけるだけだ。ああややこしい。

UltiSnipsでnvimにスニペットを導入した(vue.snippetsの例示付き)

https://github.com/SirVer/ultisnips

vueファイルを作るときに<template>とかの記述は決まり切っているのでsnippetsで解決した。 作ったファイルにvuesetupと挿入してtabキーを押すと、事前に記述しておいたコードになる。

プラグイン管理にはvim-plugを利用している。

init.vimに次の内容を書いて:PlugInstallを実行する。

" UltiSnips
Plug 'SirVer/ultisnips'
let g:UltiSnipsExpandTrigger="<tab>"

.config/nvim/UltiSnips/vue.snippetsに次の内容を記述する。

snippet vuesetup "Vue 3 Setup Script" b
<script setup lang="ts">
</script>

<template>
</template>

<style scoped lang="scss">
</style>
endsnippet

これで動く。動作に不愉快な点はいまのところない。

余談だけど<template>は真ん中にあってほしい。scriptとstyleはtemplateに依存するじゃん?scriptとstyleはお互いに依存することないじゃん?じゃあtemplateがそれぞれの間にあった方がいいよね。

『Clean Architecture 達人に学ぶソフトウェアの構造と設計』を読んだ

https://www.amazon.co.jp/Clean-Architecture-%E9%81%94%E4%BA%BA%E3%81%AB%E5%AD%A6%E3%81%B6%E3%82%BD%E3%83%95%E3%83%88%E3%82%A6%E3%82%A7%E3%82%A2%E3%81%AE%E6%A7%8B%E9%80%A0%E3%81%A8%E8%A8%AD%E8%A8%88-Robert-C-Martin/dp/4048930656

読んだので感想を書く。

ざっくり結論だけ述べると、読んでよかった。

これまで「クリーンアーキテクチャ」はその言葉の存在しか知らなかった。いや、「存在しか」は嘘だな。「ボブおじさんが考えた最強アーキテクチャ」のことだ、ということも知っていた。まあ要はTwitterや社のSlackでたまに流れてくる言葉でしかなかった。インターネットの表層を舐めて暮らしていると時折出会う言葉だ。 そのような「よく出会う言葉」の内容が知れたのがよかった。

また、この本で語られる設計思想は、長持ちするプロダクトを作るうえでいくらでも役立てそうだ。その観点においてもこの本を読んでよかった。 OSSプロダクトのコードを眺めているとcoreusecasesといったディレクトリがたまにある。それらはこの本で書かれているような設計思想が反映されたものなのだろうなあと知れた。

全編通して、「詳細」という表現が頻繁に使われている。訳注から察するに原語では"detail"だったようだ。この言語間のニュアンスの差異を受け止めてあげられればよりよい読書になるのだろう。 "detail"は核から引き剥がせ、というのがクリーンアーキテクチャの主張だろうと私は読み取った。核の概念は"detail"な実装を知る必要がない。 何が核で何が詳細かというと、アプリケーションが存在せずとも適用されるビジネスルールが核だ。 まあ言われてみれば納得する論だ。アプリケーションありきでビジネスが存在するわけではない。ビジネスを自動化するためにアプリケーションが存在する。ビジネスからしたら、アプリケーションがどのようなusecaseを持っていて、どこから入力されてどこに出力してどこにデータを保持するのかには関心がない。

いま、詳細と核が密に結びついているが故にテストが全然かけないプロダクトに向き合っているので、身に染みる本だった。

tmuxのデフォルトシェルにfishを指定したいのだけど環境によってfishのpathが異なるから困る

いわゆるdotfilesの話だ。 .tmux.confを複数環境で共有したいのだが、使いたいシェルのpathが異なる場合がある。

fishのpathが/usr/bin/fishだったり/opt/homebrew/bin/fishだったりする。前者はubuntu on wsl2で、後者はmacだ。 .tmux.confだとwhich fishの内容で動的に指定することができないので、tmuxコマンドをラップすることで解決した。

abbr tmux 'tmux set-option -g default-shell (which fish) \; new-session'

abbrなのでtmuxと打つだけで補完してくれる。which fishの結果を変数に与えながら新しいセッションを開ける。

日記: 現代における開発の様子 with AI

AIによって我々の生活は楽になるのか。ならない気がする。

エディタ上

Copilotによって単調な作業は半自動化された。 「こんな感じ!」と念じるとそれらしいものが出てくる。提案を承認するつもりでtabキーを押す。```が最後の行に書き込まれる。手動で消す。余計なもの学習しやがって。

単純な関数なら、適切な命名までつくれば2秒くらいで出力してくれる。 たまになかなか空気を読んでくれないのでコメント中の自然言語で意図を補足する。なんども書き直して提案が出てくるのをそのたびに待つ。フラストレーションがたまる。最終的に自分で書く。この時間はなんだったんだ。

あと、閉じカッコまでを出力してくれないことが多い。 jsでハンドラ関数を渡したいときとか。

element.addEventListener("click", () => {
  // () =>まで書くと、AIがブラケットの開始から関数の中身までを書いてくれるが、カッコを閉じれくれないので人間が閉じる。あるいはエディタの補完を信じる。
}

Copilot Chatは、今のところ私は使いこなせていない。 「この処理解説して」->「私はプログラミングに関する質問にしか答えられません」->「は?プログラミングの質問だが??」->「誤解を与える回答をしてしまいました、...」 これを毎回やってる。

ブラウザ上

対話AIサービスとしてはChatGPTとClaudeを使っている。どちらも課金しているのでそれぞれモデルはGPT-4とOpusだ。

コメントで意図を伝えられないときに対話AIサービスを使う。 「hogehogeな処理をするコードを書いてください。fugafugaな目的です」 ->「ほいよ」 ->「(無言でエラーログを張り付ける)」 ->「すまんやで、これが修正や」 ->一部意図と異なるので手動で修正する

複雑な処理をさせようとすると、やりとりが長くなる。最近はマシになったが、やりとりが長いと最初のコンテキストを忘れやがるのでそのたびに目的を伝えなおしていた。

あとは、エラーの原因を聞くことも多い。スタックトレースを人間が解読するのはしんどいのでAIに読ませて噛み砕かせる。

質問を雑に聞いて、回答を出させている間に検索エンジンでドキュメントを探すと、問題解決の速度と精度が高くなるが、これって人間がやるべきことが増えただけでは?

AIの苦手なこと

このような日々を1年間くらい過ごしてきて、AIが苦手なことが経験的にわかってきた。

そこそこ専門領域の技能がある人間が、その領域でぱっとできないことを聞くと、AIも同様の失敗を提示してくる。つらい。

あと、テスト駆動でコーディングすることが苦手なように思える。何度も「テスト駆動で書いて」とお願いしても本コードから書いてくる。なんなんだよ。 人間がテストコードを書いて、それをエディタ越しに読ませて、本コードを出力させる、という方がうまくいきそうが気がしてきた。

コードを出力するときに「余計なコメントをつけないで」と指示をしても聞いてくれないことが多い。 また、コメントを出させないと精度がガクンと下がる。

今のところのAIは、自分の中で基礎知識が抜け落ちているところを補完させる役割になっている。 すべてを自律的かつ専門的に作業させるのはまだできなさそうだ。

リファレンスを出させると知恵袋的なサイトを出してくることも多い。公式ドキュメントを読んでくれ。

AIサービスを自分で使用するメリット

実際に課金してよいことは、世間の評判に惑わされなくなることだ。いわゆる「驚き屋」に騙されない。 AIは、本当にしんどいところはまだ自動でやってくれない。 人間が目的と問題を丁寧に言語化して伝えてあげれば高い精度で回答を生成してくれるが、そこまで人間が言語化できている時点で解法が自明になることが多い。

ヒアドキュメントって何

なんか<<EOFとかやると複数行の文字列をスクリプト内に埋め込めるやつ、という認識である。 最近、terraformでヒアドキュメントを使って複数行にわたる文字列をパラメータや変数に渡すことが多い。これを機に調べる。

このあたりを理解したい。

概要を理解したい

ヒアドキュメントの厳密な定義や宣言があって、それをもとに各言語で実装されているものだと思い込んでいたが、どうやら違うらしいことが分かってきた。

一次情報に近そうなドキュメントを探したが見つからなかった。 個人ブログかwikipedia、あるいは世界の辞書的なサイトになろうとしているページしか見つからない。 あまり自明な概念ではないと思う。みんなが勝手に合意をしているものなのだろう。

https://en.wikipedia.org/wiki/Here_document この手の話題には珍しくwikipediaを参照させてもらう。それくらい、参考ドキュメントの発掘に困っている。

「あたかも別ファイルかのように扱える、ソースコード内の一部」だそうだ。 まあ単純に「複数行の文字列入力ができる書き方」という理解であまり間違ってなさそうに思える。

Unixシェルが起源であるという記述が複数見つかるが、信用にたるドキュメントは見つからない。

言語ごとの使われ方を知りたい

bash

https://www.gnu.org/software/bash/manual/bash.html#Here-Documents

The format of here-documents is:

[n]<<[-]word
        here-document
delimiter

最初のwordをクオートすると、here-document部分の変数が展開されないそうだ。へぇ。

他の言語でもおおむね同じっぽい。jsでは同じような概念でテンプレートリテラルがES6から導入されている。 jsに慣れている私としては、最初から「テンプレートリテラルみたいなものだよ」って説明してくれればスッと理解できたのに...。

終端識別子、自由過ぎない?

<<EOFEOF部分を何て呼ぶかが分からなかったのでChat GPTに聞いた。呼び方があっているのかは分からない。 スクリプト内で一意であればよいらしいが、あまりにも自由すぎやしないだろうか...。この自由さが、最初の理解を阻んでいた気がする。 人によってEOFEOLEOTにしているのを見る。"Enf Of File"とかそういうところから来ているのだろう。

終端識別子が自由な理由は考えてみれば明快だった。 終端識別子自体をヒアドキュメントに含められないからだ。

エスケープすればいいじゃんとなるが、特定文字だけエスケープが必要だと、「スクリプト内であたかも別ファイルかのように扱う」のニュアンスが薄れてしまうだろう。

nodejsのcanvas

https://github.com/Automattic/node-canvas

https://github.com/kesompochy/shu-chu-sen.js を作るのに使った。

ブラウザの2D CanvasAPIとほぼ同じように使える。 createCanvascanvasを作成して、getContextでctxを取得する。取得したctxで線を引いたり四角を描いたりする。

  const canvas = createCanvas(320, 320);
  const ctx = this.canvas.getContext("2d");
  loadImage("path").then(img => {
    ctx.drawImage(img, 0, 0);
  });

Bunでやろうと思っていたのだが、まだサポートされていないようだ。なのでnodejsで動かすようにした。 https://github.com/oven-sh/bun/issues/5835