いなにわうどん

うどんの話に見せかけて技術的な話をしたい(できない)

独自のプレゼンツールと卒論の進捗を共有する仕組みを作った

年の瀬ですね。先週は週に 4 回も忘年会があり、かなり良い年末を過ごしています。

さて恒例となった mast アドカレに関して、本当は銅鑼を鳴す度に NISA 口座*1へ S&P500 かオルカンが 100 円ずつ投資されるシステム*2を作り、煩悩の数だけ銅鑼を鳴らしまくってたのしく忘年!みたいな記事を書こうと思っていたのですが、諸々に忙殺している間に担当日を迎えてしまいました*3。そういうわけなので、最近軽く作ったものを紹介してお茶を濁したいと思います。

本記事は mast Advent Calendar 2024 の 23 日目の記事です。22 日目は iorin__io さんの記事「iorin.io | 未踏ITに採択された話」でした。未踏事業は本当に良い事業なのでぜひ皆さんも応募してみてください!

プレゼンツールを自作した

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 ファイル(以降、マニフェストファイルと呼称)に以下のインタフェースを満たすオブジェクトとしてスライドのメタデータを記述します。

  1. import wiss2024 from "./wiss2024";
  2. export interface Manifest {
  3. aspect: number; // アスペクト比
  4. count: number; // ページ数
  5. displaysPageIndex: boolean; // ページ番号を表示するか
  6. movies: Record<number, Movie[]>; // 動画(後述)
  7. manuscripts: string[]; // 原稿(後述)
  8. }
  9. export const manifests: Record<string, Manifest> = {
  10. wiss2024,
  11. };
スライドの遷移

横方向にスライドを並べ、①矢印キー ②横スクロール ③ページ一覧 のいずれかで遷移できるようにします。実装としては flexbox で並べて overflow-x: scroll を指定しつつ、scroll-snap-type: x mandatory; scroll-snap-align: start; によってスクロール位置を強制しています。

また、表示中のページを JS 側で管理する必要があったため、onscroll イベントの発生時に scrollLeft から現在ページを算出しています。ページを遷移させたい際には、画面幅 × ページ数 までスクロールさせます(あくまで状態はスクロール位置によって決定され、JS 側で持つページ番号はそれに付随して決定される)。

  1. const scrollPage = useCallback((i: number) => {
  2. if (!listRef.current) return;
  3. listRef.current.scroll({
  4. left: window.innerWidth * i,
  5. });
  6. }, []);
  7. const onScroll = useCallback(() => {
  8. if (!listRef.current) return;
  9. const index = Math.round(listRef.current.scrollLeft / window.innerWidth);
  10. setIndex(index);
  11. // 後略
  12. }, []);
全画面表示

Web 標準の API に、画面表示*7を全画面(フルスクリーン)にする Fullscreen API が存在しています*8。[F] キーの押下時に document.body.requestFullscreen() を呼び出すことで、ウィンドウを最大化しています。

developer.mozilla.org

動画を埋め込む

動画はマニフェストファイルに以下の通りに定義されます。角丸を指定できるのがポイントです。

  1. type Movie = {
  2. width: number;
  3. height: number;
  4. borderRadius?: number;
  5. src: string;
  6. loops: boolean;
  7. autoplay?: boolean;
  8. } & ({ top: number } | { bottom: number }) &
  9. ({ right: number } | { left: number });

これを video タグに落とし込んで再生しています。ページ遷移時に ref を探索して動画の再生時間を冒頭に移動させるとともに、autoplay 属性が true の場合は自動再生するようにしています。

  1. for (const [i, videos] of Object.entries(videoRef.current)) {
  2. for (const video of videos) {
  3. if (!video.current) continue;
  4. if (parseInt(i) === index) {
  5. video.current.currentTime = 0;
  6. if (video.current.autoplay) {
  7. video.current.play();
  8. }
  9. } else {
  10. video.current.pause();
  11. }
  12. }
  13. }
発表者ツール

苗場に行く前日に発表練習をしたところ、10 分の発表時間に対して 11 分半も掛かっていたことに気が付いたため、急遽徹夜で原稿と発表者ツールを用意しました。[W] キーを押すことで、サブウィンドウが開いて原稿の内容が表示されるようにします。また、[S] キーを押すことでサブウィンドウ内で時間計測を行います。

これができます

以下に示すコードのように、原稿の内容をマニフェストファイルに定義します。

  1. const wiss2024: Manifest = {
  2. manuscripts: [
  3. `1. ページ目の原稿`,
  4. `2. ページ目の原稿`,
  5. ],
  6. // (省略)
  7. }

この際、Window.postMessage() 関数を使用するとウィンドウ間でメッセージを送信することができるため、window.open() 関数を用いてウィンドウを開いた後、親ウィンドウからページ番号を子ウィンドウに送信します。子ウィンドウ側は、受信したページ番号に応じて対応する原稿を表示します。

  1. // 親ウィンドウ側
  2. const [childWindow, setChildWindow] = useState<Window | null>(null);
  3. setChildWindow(window.open(`/manuscript/${name}`));
  4. // 子ウィンドウにメッセージを送信
  5. if (childWindow) {
  6. childWindow.postMessage(
  7. {
  8. action: "SyncMessage",
  9. message: index,
  10. }, "*");
  11. }
  12. // 子ウィンドウ側
  13. window.addEventListener("message", (e) => {
  14. switch (e.data.action) {
  15. case "SyncMessage":
  16. setIndex(parseInt(e.data.message));
  17. }
  18. });

感想/改善点

当日は特にバグもなく無事動き、発表もそつなく終わりました。内心ヒヤヒヤしていた*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 年も残すところ僅かとなりました。みなさん、どうぞ良いお年をお迎えください!

*1:今年、NISA は 63 万円ほど投資して 7,000 円程度の利益しかでていない

*2:銅鑼はインタフェースとしての役割を担っているため、投資先は叩く位置によって当然変わる

*3:学部 4 年になってから本当に余裕がなく個人開発に手が回っていない

*4:発表あるある:手元の PC と HDMI 端末の相性が悪い

*5:発表あるある:ネットが繋がらなくなる

*6:PDF に書き出して PDF.js で読み込んでも良かったかも

*7:Fullscreen API は当該タブを最大化するのみならず、要素を全画面にすることもできます

*8:恥ずかしながら初めて知った

*9:自作ツールあるある:デモで動かない

*10:アカウント登録ページだけ静的アセットとして配信しており、SPA もどきの謎構成になってしまった

*11:卒業のモチベは「筑波大学を卒業しました」というエントリを書くこと以外にない

*12:curl を叩けるかの試金石になっている

*13:先程話題に上がった WISS の記念講演で増井先生(フリック入力や Scrapbox の開発者)がお話をされていました。増井先生の有名なエントリに「自分が使わないものを発表するな」というエントリがあり、まさしくその通りだなと感じています

スライドに適した「ニュートラルでデフォルト感のない」フォントを考える

先日,深夜に友人と話している際に「スライドのフォントに結局 Noto Sans(≒ 源ノ角ゴシック)を選んでしまう」という話題が出ました*1.Noto Sans がオープンソースであり,Google スライド等のアプリケーションで最初から使用できるという理由も勿論あるのですが,個人的にはそれだけが理由でないように感じます.

ときに,プレゼンテーション用のスライドにはどのような書体が適しているのでしょうか? 遠くから見えるように,視認性が高いフォントを使いましょう! ――という教科書的な回答はさておき,これは結構難しい問題に思えます.というのも(特に,洒落た発表ではなく研究発表のようにお堅い)スライドは,視認性のほかに,ある主の無味乾燥さが求められるように感じるからです.個人的には,「視認性」「ニュートラルさ」「デフォルト感がない」の 3 つを兼ね備えた書体が適切であると感じています.このあたりに関して雑感を書いてみます.

*1:Morisawa Fonts,LETS,Adobe Fonts の書体群も使用可能な環境の中,あえて

続きを読む

「安達としまむら」聖地巡礼記(岐阜編)

先月に旅行がてら「安達としまむら」の聖地巡礼に行ってきました。同作では女子高生である安達としまむらの尊き日常が、岐阜県本巣市を舞台に描かれます。岐阜・名古屋の聖地を一日掛けて丁寧に取材してきたので、写真とともにお伝えしていければと思います。

続きを読む

2023 年を振り返って

年の瀬ですね。今年も 12/30、31 と東京ビッグサイト(東京都江東区)で開催されたコミックマーケット 103 に参戦してきました。忘年会と冬コミ参戦が連続する非常に良い年末を過ごせたと感じています。

――2023年が終わります。大晦日は一年の振り返りをするのが恒例*1となりつつあるので、待機列に並びながら昨年同様、本エントリを書き上げています。振り返り記事を後で見返す機会は意外にも多く、折に触れて自分は昨年末に何を考えて/何を目標としていたかを参照する際の便利な道標となるものです*2。

*1:どうも私は例年同じ行動を取りたがる傾向にあるらしい

*2:春に軽く書いたエントリと被る点もあるので、物好きの方はそちらも参照してみてください

続きを読む

雙峰祭でそば焼いたらやきそばになってワロタ

本記事は、mast Advent Calendar 2023*1 の 23 日目の記事です。22 日目はびきさんの記事「こういうところ、好き!|びき」でした。横浜近辺のエモい写真が沢山出てくるので必読です。彼はめちゃくちゃ良い写真を撮るな〜と一年次の頃から思っていて、気が付けば自分も富士フイルムのカメラをポチっていました。

 

年の瀬ですね。こんにちは、いなにわうどんです。突然ですが、今年の 11 月 3, 4 日に開催された筑波大学学園祭「雙峰祭」にて、私は有志団体として模擬店「驚額の殿堂」を出店し、友人らとやきそばを調理して 150 円で販売しました。

企画を実施するにあたって、我々は「美味しいやきそばを焼いて、とにかく安く売ること」を第一の目標に定めて準備を進めてきました。その結果、当日は大盛況のうちに幕を閉じ、目標は十分に達成されたと思います。本記事では、雙峰祭にやきそば屋を出店するまでの経緯や準備過程、当日の様子、得られた反響などを企画代表者の視点からお伝えしていきます。

GitHub Actions を回してピザを頼みたい

年の瀬ですね。クリスマスの足音も近く、ピザなんかを頼んだら景気が良いかなと思ったので、GitHub 上で Issues を生やすとピザが頼める仕組み(workflows)を構築してみました。

本記事は mast Advent Calendar 2023 の 7 日目の記事です。6 日目は Hitoko T. 先生の記事「我が家に猫3匹がやって来た話|Hiroko T.」でした。猫、癒やしですよね

折角のアドカレの機会ですから、GitHub 上でピザを頼むまでの過程を、GitHub や Web 技術、ピザ等に明るい方にも、そうでない方にもお楽しみいただけるように説明*1*2を進めていきます*3。少し長くなりますが、どうぞお付き合いください。

*1:初学者向けの完全な解説は流石に難しいので、雰囲気だけでも掴んでいただければという趣旨です

*2:技術的に不正確な記述があるかもしれませんが許してんぽ〜

*3:Zenn に書くのもアレだなあと思ってはてなブログに書いた

続きを読む

学園祭で売上をリアルタイムに公開するサイトを雑に作ると盛り上がる

先日の学園祭で友人のオタク達とやきそばを焼いて原価ギリギリで売ったところ予想以上の盛況でした*1。色々と工夫点はあったのですが、その一つとして売上杯数を Web 上で登録してリアルタイムで雑に public internet に公開するという試みをしてみところちょっと盛り上がったため、その経緯を書いていきたいと思います*2。

*1:一時は 4 列並びとかになっていてどこの壁サー?という感じだった

*2:最近は学園祭での IT を Zenn 等に書くことが流行っているっぽい

続きを読む