年の瀬ですね。先週は週に 4 回も忘年会があり、かなり良い年末を過ごしています。
さて恒例となった mast アドカレに関して、本当は銅鑼を鳴す度に NISA 口座*1へ S&P500 かオルカンが 100 円ずつ投資されるシステム*2を作り、煩悩の数だけ銅鑼を鳴らしまくってたのしく忘年!みたいな記事を書こうと思っていたのですが、諸々に忙殺している間に担当日を迎えてしまいました*3。そういうわけなので、最近軽く作ったものを紹介してお茶を濁したいと思います。
プレゼンツールを自作した
12 月中旬に開催された WISS(インタラクティブシステムとソフトウェアに関するワークショップ)という学会(ワークショップ)に参加してきました。苗場に 2 泊 3 日泊まり込みで HCI 分野の研究が発表されるというもので、登壇発表、デモ発表、夜の懇親会のどれを取っても非常に興味深く、楽しい経験になりました。また、有り難いことに卒研として取り組んでいる文字入力手法の開発を報告した論文が採択され、登壇発表を行うことになりました。
さて、プレゼンはプレゼンツールから作ると良いとされているため、簡易的なプレゼンツールを実装して実際に活用してみました。本ツールは Web アプリケーションとして作動するため、他の PC からも簡単に利用できるほか*4、localhost で起動すればオフライン環境でも作動します*5。以下の URL から実際に動くスライドを閲覧することができます。
https://slide.yokohama.dev/wiss2024
基本的な操作を以下に示します。
操作 | 内容 |
---|---|
スクロール、[←][→] キー | ページ戻し/送り |
[F] キー | 全画面表示 |
[W] キー | 原稿(発表者ツール)を開く |
[C] キー | ポインタを表示 |
[L] キー | ページ一覧を表示 |
実装
お馴染みの技術構成である Vite + React + TypeScript を用いて実装し、Cloudflare Pages にデプロイします。
スライド自体は Illustrator で予め作成しておき、webp 形式の画像として出力しておきます*6。また、.ts ファイル(以降、マニフェストファイルと呼称)に以下のインタフェースを満たすオブジェクトとしてスライドのメタデータを記述します。
import wiss2024 from "./wiss2024"; export interface Manifest { aspect: number; // アスペクト比 count: number; // ページ数 displaysPageIndex: boolean; // ページ番号を表示するか movies: Record<number, Movie[]>; // 動画(後述) manuscripts: string[]; // 原稿(後述) } export const manifests: Record<string, Manifest> = { wiss2024, };
スライドの遷移
横方向にスライドを並べ、①矢印キー ②横スクロール ③ページ一覧 のいずれかで遷移できるようにします。実装としては flexbox で並べて overflow-x: scroll
を指定しつつ、scroll-snap-type: x mandatory; scroll-snap-align: start;
によってスクロール位置を強制しています。
また、表示中のページを JS 側で管理する必要があったため、onscroll イベントの発生時に scrollLeft から現在ページを算出しています。ページを遷移させたい際には、画面幅 × ページ数 までスクロールさせます(あくまで状態はスクロール位置によって決定され、JS 側で持つページ番号はそれに付随して決定される)。
const scrollPage = useCallback((i: number) => { if (!listRef.current) return; listRef.current.scroll({ left: window.innerWidth * i, }); }, []); const onScroll = useCallback(() => { if (!listRef.current) return; const index = Math.round(listRef.current.scrollLeft / window.innerWidth); setIndex(index); // 後略 }, []);
全画面表示
Web 標準の API に、画面表示*7を全画面(フルスクリーン)にする Fullscreen API が存在しています*8。[F] キーの押下時に document.body.requestFullscreen()
を呼び出すことで、ウィンドウを最大化しています。
動画を埋め込む
動画はマニフェストファイルに以下の通りに定義されます。角丸を指定できるのがポイントです。
type Movie = { width: number; height: number; borderRadius?: number; src: string; loops: boolean; autoplay?: boolean; } & ({ top: number } | { bottom: number }) & ({ right: number } | { left: number });
これを video タグに落とし込んで再生しています。ページ遷移時に ref を探索して動画の再生時間を冒頭に移動させるとともに、autoplay 属性が true の場合は自動再生するようにしています。
for (const [i, videos] of Object.entries(videoRef.current)) { for (const video of videos) { if (!video.current) continue; if (parseInt(i) === index) { video.current.currentTime = 0; if (video.current.autoplay) { video.current.play(); } } else { video.current.pause(); } } }
発表者ツール
苗場に行く前日に発表練習をしたところ、10 分の発表時間に対して 11 分半も掛かっていたことに気が付いたため、急遽徹夜で原稿と発表者ツールを用意しました。[W] キーを押すことで、サブウィンドウが開いて原稿の内容が表示されるようにします。また、[S] キーを押すことでサブウィンドウ内で時間計測を行います。
以下に示すコードのように、原稿の内容をマニフェストファイルに定義します。
const wiss2024: Manifest = { manuscripts: [ `1. ページ目の原稿`, `2. ページ目の原稿`, ], // (省略) }
この際、Window.postMessage() 関数を使用するとウィンドウ間でメッセージを送信することができるため、window.open() 関数を用いてウィンドウを開いた後、親ウィンドウからページ番号を子ウィンドウに送信します。子ウィンドウ側は、受信したページ番号に応じて対応する原稿を表示します。
// 親ウィンドウ側 const [childWindow, setChildWindow] = useState<Window | null>(null); setChildWindow(window.open(`/manuscript/${name}`)); // 子ウィンドウにメッセージを送信 if (childWindow) { childWindow.postMessage( { action: "SyncMessage", message: index, }, "*"); } // 子ウィンドウ側 window.addEventListener("message", (e) => { switch (e.data.action) { case "SyncMessage": setIndex(parseInt(e.data.message)); } });
卒論の進捗を共有する仕組みを作った
続いての話題です。表題の通り、卒論(修論も含む)の進捗を共有する Web サイトを作りました。下記の URL からアクセスできます。実装には Hono を使用しています*10。
https://sotsuron.yokohama.dev/
私も学部 4 年となり、来春には筑波大学を卒業*11する運びとなりました。ところで卒業をするには卒論を書かねばならず、年末年始も休みなく LaTeX とにらめっこしています(メ創の卒論締切は 2/3)。この苦行を少しでもエンタメ性のあるものにしたいと着想したのが理由です。
とはいえ、進捗を生む度に逐次 Web サイトを GUI から更新するのは面倒なので、情報更新用のエンドポイントのみを用意し、API を叩いて更新してもらうことにしました*12。理系であれば LaTeX を使用すると思うので、latexmk や Git Hooks に curl コマンドをにゅっと忍ばせておけば更新も自動化できます。DX ですね!
忘年会の場で sotsuron.yokohama.dev!と連呼することで既に何人かの友人に使ってもらえたのですが、「Docker 環境には pdfinfo がないので動かない」「章ごとに分割して書いているので数ページしか進捗がないと勘違いされてしまう」といった問題も発生しているようです。これらについても、空いた時間に対応していければと思います。なお、ソースコードは以下の GitHub に公開しています。
むすびにかえて
今年は卒研に追われて個人開発に取り組む時間があまり取れず、加えて研究領域も実装がそれほど重視されない分野に進んでしまったため、全体的に開発から遠ざかった一年となりました。最近は(特に書き捨てるようなプログラムの場合)ChatGPT にコードを生成してもらうことも多いのですが、それでもやはり自分でコードを書く行為は楽しいので、来年は上手く時間を捻出しつつ、自分で使いたくなるもの*13を楽しく作っていきたいなと思っています。
2024 年も残すところ僅かとなりました。みなさん、どうぞ良いお年をお迎えください!
*1:今年、NISA は 63 万円ほど投資して 7,000 円程度の利益しかでていない
*2:銅鑼はインタフェースとしての役割を担っているため、投資先は叩く位置によって当然変わる
*3:学部 4 年になってから本当に余裕がなく個人開発に手が回っていない
*4:発表あるある:手元の PC と HDMI 端末の相性が悪い
*5:発表あるある:ネットが繋がらなくなる
*6:PDF に書き出して PDF.js で読み込んでも良かったかも
*7:Fullscreen API は当該タブを最大化するのみならず、要素を全画面にすることもできます
*8:恥ずかしながら初めて知った
*9:自作ツールあるある:デモで動かない
*10:アカウント登録ページだけ静的アセットとして配信しており、SPA もどきの謎構成になってしまった
*11:卒業のモチベは「筑波大学を卒業しました」というエントリを書くこと以外にない
*13:先程話題に上がった WISS の記念講演で増井先生(フリック入力や Scrapbox の開発者)がお話をされていました。増井先生の有名なエントリに「自分が使わないものを発表するな」というエントリがあり、まさしくその通りだなと感じています