はじめに
皆さんは Mousecape というものを使ったことがあるでしょうか?
Windowsでは .ani ファイルでできるアニメーションするカーソルを Mac でも使えるようにしてくれるアプリです。

カーソルの種類が少ない件
しかしこのアプリ、要求するファイルが .cape という .ani とは中身が違うものになっています。
そもそも、 Mac を使うような真面目な開発者たちは、私のようにカーソルにこだわったりしないはずなので需要がなく、あまり .cape の種類もありません。
そこで、『ani2capeコンバーター』なるものを探したのですが、いいものが見つかりませんでした。
(あるにはあったのですが、英語ではない言語で書かれていて読めなかったので諦めました。)
ということで、なければ AI に書かせればいい精神で作ってみました。
完成品
そして出来上がったものが以下の通りになります。
何かしらの、 .ani を変換している最中のものを貼りたかったのですが著作権が怖いので諦めました。
.ani ファイルを放り込めば使えるはずです。

Github: https://github.com/Aotumuri/ani2cape_site
Home page: https://aotumuri.github.io/ani2cape_site/
作るにあたって発生した問題
そもそも .ani ファイルはどのような構造になっているのか。
ファイルを分解して構造を調べるわけにもいかないので WIKI を参考にしました。
ざっくり書くと以下の要素が入っています。
- カーソルの名前
- 作者情報
- デフォルトのフレームレート
- シーケンス情報
- カーソルのホットスポット
- 個々のフレーム
- 個々のフレームレート
ここをみると、 .ani ファイルにはその絵がどの種類のカーソルの絵に対応するのかが書いてありません。
私はてっきりここに対応が書いてあり、それを元に自動で適応していると思っていていたので困りました。
.cape では、各ファイルがどのカーソルの絵と対応するのかを明記する必要があります。
そのため、いちいち最初にどの絵がどれに対応するかを入力する必要が出てきたのです。
最初は、 python を使ってコマンド一つで完了するものを想定していました。
しかしそれを諦め、 html + js + css のサイト形式にしてもらいました。
そこで入力をして一つのファイルで出力するようになっています。
関係ないのですが、Windows で使う際にどのように適応してるのでしょうね。
.cape は一ファイルにまとまっているので適応は楽ですが、 .ani は複数ファイルに分裂しています。
一つ一つ適応していくのでしょうか?
おわりに
結果としては、なかなか満足のいくツールになったと思います。
実際にアニメーションしながら作業できる環境は、やはり開発のモチベーションが上がりますね。
まあ、人によっては気が散ると言うかもしれませんが。
結局、数日で元に戻してしまいましたし...
ここまで読んでくださり、ありがとうございました。
おまけ
AI が書いてくれたとんでもないコードを共有しておきます。
プロンプトでは『各ANIファイルの対応キーをリストから選べるようにしてください』とだけ指示したのですが、いつの間にか“ファイル名から対応キーを推測する”という機能まで勝手に付けてきました。
しかも、思いついた単語を片っ端から検索して判定するという力技...
const CURSOR_SEARCH_INDEX = (() => {
const items = [];
for (const [k, label] of Object.entries(CURSOR_NAME_MAP)) {
const tokens = new Set();
// from label words
for (const t of (label || "")
.toLowerCase()
.split(/[^a-z0-9]+/)
.filter(Boolean)) {
tokens.add(t);
}
// from key tail (e.g., com.apple.coregraphics.Arrow -> 'arrow')
const tail = k.split(".").pop();
if (tail) tokens.add(tail.toLowerCase());
// direction expansions for common patterns in labels
const lbl = (label || "").toLowerCase();
if (/w[-–—]?e/.test(lbl)) {
tokens.add("we");
tokens.add("ew");
tokens.add("leftright");
tokens.add("horizontal");
}
if (/n[-–—]?s/.test(lbl)) {
tokens.add("ns");
tokens.add("sn");
tokens.add("updown");
tokens.add("vertical");
}
if (/nw[-–—]?se/.test(lbl)) {
tokens.add("nwse");
}
if (/ne[-–—]?sw/.test(lbl)) {
tokens.add("nesw");
}
// normalize some common aliases
if (tokens.has("ibeam")) {
tokens.add("text");
tokens.add("caret");
}
if (tokens.has("arrow")) {
tokens.add("default");
tokens.add("normal");
}
if (tokens.has("pointing")) {
tokens.add("hand");
tokens.add("pointer");
}
if (tokens.has("forbidden")) {
tokens.add("notallowed");
tokens.add("no");
tokens.add("stop");
}
if (tokens.has("busy") || tokens.has("wait")) {
tokens.add("spinner");
}
if (tokens.has("crosshair")) {
tokens.add("cross");
}
items.push({ key: k, tokens });
}
return items;
})();