年の瀬ですね。先週は週に 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()
を呼び出すことで、ウィンドウを最大化しています。
developer.mozilla.org
動画を埋め込む
動画はマニフェスト ファイルに以下の通りに定義されます。角丸を指定できるのがポイントです。
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));
}
} );
感想/改善点
当日は特にバグもなく無事動き、発表もそつなく終わりました。内心ヒヤヒヤしていた*9 ので良かったです…… 気づいた改善点としては、以下の点があります。
ページ番号は Web 側で表示するではなく元スライド側に埋めておくと良い 質疑応答等の際に資料を参照して「何ページ」と指し示したい場合があるため
画面の背景は白ではなく黒にすると良い アスペクト比 が合わなかった際に誤魔化せるため
資料を更新したら Cloudflare のキャッシュをパージする必要がある
発表
卒論の進捗を共有する仕組みを作った
続いての話題です。表題の通り、卒論(修論 も含む)の進捗を共有する Web サイトを作りました。下記の URL からアクセスできます。実装には Hono を使用しています*10 。
https://sotsuron.yokohama.dev/
トップが 31 ページで早くも圧力を掛けられている
私も学部 4 年となり、来春には筑波大学 を卒業*11 する運びとなりました。ところで卒業をするには卒論を書かねばならず、年末年始も休みなく LaTeX とにらめっこしています(メ創の卒論締切は 2/3)。この苦行を少しでもエンタメ性のあるものにしたいと着想したのが理由です。
とはいえ、進捗を生む度に逐次 Web サイトを GUI から更新するのは面倒なので、情報更新用のエンドポイントのみを用意し、API を叩いて更新してもらうことにしました*12 。理系であれば LaTeX を使用すると思うので、latexmk や Git Hooks に curl コマンドをにゅっと忍ばせておけば更新も自動化できます。DX ですね!
忘年会の場で sotsuron.yokohama.dev!と連呼することで既に何人かの友人に使ってもらえたのですが、「Docker 環境には pdfinfo がないので動かない」「章ごとに分割して書いているので数ページしか進捗がないと勘違いされてしまう」といった問題も発生しているようです。これらについても、空いた時間に対応していければと思います。なお、ソースコード は以下の GitHub に公開しています。
github.com
むすびにかえて
今年は卒研に追われて個人開発に取り組む時間があまり取れず、加えて研究領域も実装がそれほど重視されない分野に進んでしまったため、全体的に開発から遠ざかった一年となりました。最近は(特に書き捨てるようなプログラムの場合)ChatGPT にコードを生成してもらうことも多いのですが、それでもやはり自分でコードを書く行為は楽しいので、来年は上手く時間を捻出しつつ、自分で使いたくなるもの*13 を楽しく作っていきたいなと思っています。
2024 年も残すところ僅かとなりました。みなさん、どうぞ良いお年をお迎えください!