🗣️

【ライブコーディング資料公開】ペチオブLT会で発表しました

に公開

2025/08/07に開催されたペチオブLT会で「CoderDojoで小学生にめっちゃウケたp5.jsライブコーディング」というタイトルでLT発表をしました。

https://phper-oop.connpass.com/event/359966/

youtube配信が行われる予定だったのですが、残念ながら会場機材トラブルで配信がされなかったため完全オフラインの会でした。

ライブコーディングだったのでスライド資料等はありません。以下に全リソースおよび環境構築方法、使用した発表台本を示します。手順に沿って動かしていけば誰でも同じ発表ができるはずです。

タイトル画像

スライドっぽく見せたのはスタートの1枚のみです。Macのプレビューアプリで画面に大写しにして、開始後に閉じました。


タイトル画像

今更ですが名前を書いていないことに気づきました。発表中に自己紹介できる時間の余裕は全く無かったのですが、名前を書くくらいはしておくべきでしたね。

demo環境構築

p5.jsの実行環境が必要です。p5.vscodeのtemplateディレクトリにライブラリと最低限のスケッチが保存されているので、これをダウンロードします。ここはスクリプト等は用意せず手動で行いました。

https://github.com/antiboredom/p5.vscode/tree/master/template

適当なディレクトリにそのまま展開します。

~/projects/p5js-lt
├─ libraries/
│  ├─ p5.min.js
│  └─ p5.sound.min.js
├─ index.html
├─ sketch.js
└─ style.css

発表一回だけのためならこれだけでよいのですが、何度も初期化して練習できるよう、sketch.jsはtemplateディレクトリに残しておきました。

 ~/projects/p5js-lt
 ├─ libraries/
 │  ├─ p5.min.js
 │  └─ p5.sound.min.js
+├─ template/
+│     └─ sketch.js # デフォルトのsketch.js
 ├─ index.html
 ├─ sketch.js
 └─ style.css

発表用にtemplate/sketch.jsをちょっと編集しておきます。コメントと1行目の空行を追加しました。

template/sketch.js
+
+// 初期化処理 1回だけ実行
 function setup() {
   createCanvas(400, 400);
 }

+// 描画処理 毎秒60回実行
 function draw() {
   background(220);
 }

ブラウザで表示するために適当なサーバーを建てます。筆者はbunとlive-serverを使いました。

bun init
bun add -d live-server

bun initで以下のようなファイルが作られます。

 ~/projects/p5js-lt
 ├─ libraries/
 │  ├─ p5.min.js
 │  └─ p5.sound.min.js
+├─ node_modules/
 ├─ template/
 │     └─ sketch.js
+├─ .gitignore
+├─ bun.lock
 ├─ index.html
+├─ package.json
+├─ README.md
 ├─ sketch.js
 ├─ style.css
+└─ tsconfig.json

package.jsonに以下の通り起動用のscriptを定義します。
発表で使うのはbun devです。sketch.jsを初期化し、サーバーが起動し、sketch.jsの変更時に表示が更新されます。

package.json
{
  "name": "p5js",
  "private": true,
  "scripts": {
    "init:demo": "cp -f template/sketch.js sketch.js",
    "serve:demo": "live-server --port=3000 --watch=sketch.js,index.html",
    "dev": "bun init:demo && bun serve:demo"
  },
  "devDependencies": {
    "@types/bun": "latest",
    "@types/p5": "^1.7.6",
    "live-server": "^1.2.2"
  },
  "peerDependencies": {
    "typescript": "^5"
  }
}

これでライブコーディング発表環境が整いました。

ライブコーディングスクリプト

実際に喋りながら手を動かすと緊張でうまく喋れないもしくは正しく打ち込めないことが明らかなので、Vimを自動操縦して入力することにしました。幸い、必要なプラグインは筆者自身が開発済みです。

https://zenn.dev/kawarimidoll/articles/f80e2194303564

https://github.com/kawarimidoll/autoplay.vim

kawarimidoll/autoplay.vimは普通にパッケージマネージャで読み込めばOKです。しかし、普段のvimrcで使うものでもないので、筆者はghqでクローンして、必要なときにだけ読み込む(直接runtimepathを記述する)ようにしています。

自動実行の台本はscenario.vimに定義します。以下で使用しているパスは絶対パスで書かれているので、使用の際は手元のパスに書き換えてください。

scenario.vim
set runtimepath+=/Users/kawarimidoll/ghq/github.com/kawarimidoll/autoplay.vim

set cursorline cursorcolumn

let s:scripts_list = [
      \ [
      \   { 'call': 'cursor', 'args': ['1', '1'] },
      \   "Oconst width = 800;\<cr>const height = 800;\<esc>",
      \ ], [
      \   { 'call': 'search', 'args': [','] },
      \   { 'exec': "s/400, 400/" },
      \   { 'call': 'search', 'args': [')'] },
      \   "iwidth, height\<esc>",
      \ ], [
      \   { 'call': 'search', 'args': ['background'] },
      \   "o\<cr>circle(0, 0, 30);\<esc>",
      \ ], [
      \   { 'exec': "%s/ 30/ 150" },
      \ ], [
      \   { 'exec': "%s/ 150/ 30" },
      \ ], [
      \   { 'call': 'cursor', 'args': ['1', '1'] },
      \   { 'call': 'search', 'args': ['circle'] },
      \   "ocircle(20, 20, 30);\<cr>circle(40, 40, 30);\<cr>circle(60, 60, 30);\<esc>",
      \ ], [
      \   { 'exec': "g/circle/d" },
      \ ], [
      \   { 'call': 'cursor', 'args': ['$', '1'] },
      \   "Oconst num = 6;\<cr>for (let i = 1; i <= num; i += 1) {\<cr>",
      \   "const d = i * 9;\<cr>const x = d;\<cr>const y = d;\<cr>circle(x, y, 30);\<cr>",
      \   "}\<esc>",
      \ ], [
      \   { 'exec': "%s/num = 6;/num = 60;" },
      \ ], [
      \   { 'call': 'cursor', 'args': ['1', '1'] },
      \   { 'call': 'search', 'args': ['createCanvas'] },
      \   "o\<cr>colorMode(HSB, 360, 100, 100);\<cr>noFill();\<cr>strokeWeight(4);\<esc>",
      \   { 'exec': "%s/22/", 'wait': 100 },
      \   { 'call': 'cursor', 'args': ['1', '1'] },
      \   { 'call': 'search', 'args': ['for'] },
      \   "ostroke((i * 15) % 360, 50, 100);\<cr>\<esc>",
      \ ], [
      \   { 'call': 'cursor', 'args': ['1', '1'] },
      \   { 'call': 'search', 'args': ['const num'] },
      \   "Oconst theta = 1;\<esc>",
      \ ], [
      \   { 'call': 'cursor', 'args': ['1', '1'] },
      \   { 'call': 'search', 'args': ['const x = d', 'e'] },
      \   "a * cos(theta)\<esc>",
      \   { 'call': 'search', 'args': ['const y = d', 'e'] },
      \   "a * sin(theta)\<esc>",
      \ ], [
      \   { 'call': 'cursor', 'args': ['1', '1'] },
      \   { 'call': 'search', 'args': ['background'] },
      \   "o\<cr>translate(width / 2, height / 2);\<esc>",
      \ ], [
      \   { 'call': 'cursor', 'args': ['1', '1'] },
      \   { 'call': 'search', 'args': ['theta = 1', 'e'] },
      \   "a\<bs>frameCount\<esc>",
      \ ], [
      \   "a / 1000\<esc>",
      \ ], [
      \   { 'call': 'cursor', 'args': ['1', '1'] },
      \   { 'call': 'search', 'args': ['for'] },
      \   "oif(i % 15 !== 0) continue;\<esc>",
      \ ], [
      \   { 'call': 'search', 'args': ['(theta)', 'e'] },
      \   "i * i\<esc>",
      \   { 'call': 'search', 'args': ['(theta)', 'e'] },
      \   "i * i\<esc>",
      \ ], [
      \   { 'call': 'cursor', 'args': ['1', '1'] },
      \   { 'call': 'search', 'args': ['if'] },
      \   { 'exec': "d" },
      \ ] ]

" Reserve all scripts with numbered names
for i in range(len(s:scripts_list))
  call autoplay#reserve({
        \ 'name': 'phase_' .. (i + 1),
        \ 'wait': 40,
        \ 'spell_out': v:true,
        \ 'remap': v:false,
        \ 'scripts': s:scripts_list[i],
        \ })
endfor

let g:autoplay_count = 0
function! RunAutoplayByCount() abort
  let g:autoplay_count = v:count > 0 ? v:count : (g:autoplay_count + 1)
  if g:autoplay_count > len(s:scripts_list)
    echo 'Autoplay finished!'
    return
  endif
  let phase_name = 'phase_' .. g:autoplay_count
  call autoplay#run(phase_name)
  echo phase_name
endfunction

nnoremap <cr> <cmd>call RunAutoplayByCount()<cr>
nnoremap ; <cmd>write<cr>

function! s:init_p5_demo() abort
  call system('cp -f /Users/kawarimidoll/projects/p5js-lt/template/sketch.js /Users/kawarimidoll/projects/p5js-lt/sketch.js')
endfunction
command! InitP5Demo call s:init_p5_demo() | edit! | redraw

colorscheme zellner

このファイルは以下を定義します。

  • <cr>でautoplayによる自動編集
  • ;でファイルを保存
  • 編集箇所をわかりやすくするため、行と列をハイライト
  • カラースキームを設定
  • 練習用に、ファイルを初期化してリロードする:InitP5Demoコマンドを定義(sketch.jsを開いていることを想定)

最終的にはsketch.jsを以下の形に書き換えます。

sketch.js最終結果
const width = 800;
const height = 800;

// 初期化処理 1回だけ実行
function setup() {
  createCanvas(width, height);

  colorMode(HSB, 360, 100, 100);
  noFill();
  strokeWeight(4);
}

// 描画処理 毎秒60回実行
function draw() {
  background(0);

  translate(width / 2, height / 2);

  const theta = frameCount / 1000;
  const num = 60;
  for (let i = 1; i <= num; i += 1) {
    stroke((i * 15) % 360, 50, 100);

    const d = i * 9;
    const x = d * cos(theta * i);
    const y = d * sin(theta * i);
    circle(x, y, 30);
  }
}

発表

発表時間は5分ですが、小規模イベントだったので厳しい時間制限はなく、前説は比較的ゆっくり読みました。とはいえ、曲が始まってしまうと途中で止まれないため、編集を見せる部分は引き延ばせません。
「えーっと」みたいなフィラーを可能な限りなくしたかったのでかなり細かく台本を書きました。

起動

ターミナルでbun devしてブラウザの画面を表示させます。
サーバーとは別タブや別ウィンドウでvi scenario.vim sketch.jsしてライブコーディングスクリプトと編集対象のファイルを開き、:sourceしてscenario.vimを読み込んだあと、sketch.jsにバッファを切り替えます。
ここまでできたらタイトル画像を開いて最大化しておきます。

前説

ここに使える時間は30秒くらいです。実際はもう少しゆっくり読んだはずです。

Hi guys カワリミ人形と申します。
本日は「CoderDojoで小学生にめっちゃウケたp5.jsライブコーディング」というタイトルで発表させていただきますので恐み恐みもよろしくお願いいたします。
普通にやると10分くらいかかるので早口になったり省略したりしてますがご容赦願います。
p5.jsというのはJavaScriptでお絵描きできるライブラリで、これを使って変数とループをテーマに説明しました。小学生対象でしたが、プログラミングの基礎解説という感じで、子供だましというわけではないので皆さんにも楽しんでいただけると思います。

以上を読みつつタイトル画像を消し、p5.jsの公式ページとCoderDojoの解説ページをザッピングするように見せます。

https://p5js.org/

https://ja.wikipedia.org/wiki/コーダー道場

その後、レンダリング結果の画面を表示させます。

こちらに見えているのが実際のコードと実行結果です。上のsetup関数が初期化処理で、今は画面サイズを定義してます。下のdraw関数は描画処理で、毎秒60回呼ばれます。いまは背景に色を付けています。これを実行すると、グレーの画面になります。

では、5分の発表なのでタイマーを掛けますね。

タイマーをかけると言って音楽を再生します。

BGM再生

Perfumeの「ねぇ」です。
https://www.youtube.com/watch?v=nbeGeXgjh9Q

Jasrac管理なのでYoutube配信も可能という調査の上での選曲でしたが、結局は配信ナシだったので気にしなくても良かったかもしれません。

https://www2.jasrac.or.jp/eJwid

最初の「2人の思い出を重ねて」を歌い切るまで待ちます。

メイントーク

冒頭のフレーズを歌い切ったら喋ります。この冒頭の待機時間が15秒ほどあり、最終結果をノータッチで見せる時間が確実に2分弱かかります。曲が4分20秒くらいなので、以下の台本を2分ちょいで読み切る必要があります。

明記してありませんが、適当なところで<cr>での編集と;での保存を行って画面を更新していきます。
「説明」と「書き換え」はある程度オーバーラップできるのですが、それを「表示」させるときには少し間を空けて、どう編集したのかを理解してもらうようにします。

はい、ここから入りますね。
まず、この画面サイズを決めているcreateCanvasの引数を変数にしてみます。
幅と高さの変数を定義してここに渡します。
もともと400だったのを800にしました。こうすると?
こうなります。大きくなりました。
こうやってコードが画面に反映されます。次は図形を描いてみます。
circleと言う関数を使います。なんとなく何をするかわかりますね。実行してみましょう。
はい、ゼロ・ゼロの位置に円を描けました。見えますね。
見やすくするために半径を大きくしてみましょう。
はいここにありました。原点が左上にあることがわかります。
一旦、サイズを元に戻して、次はこの円をたくさん描いてみます。こうやって座標を変えつつ何度も書いても良いのですが、これはプログラミングぽくないよという話をしました。
どうするかというと、ループを使います。インデックスを座標に使って複数の円を描いていきます。
こうすると、circleの使用は1つだけで、たくさん円を描けます。
ループの良いところは、個数を変えることで、簡単に数を変えられることです。
さて、ちょっとわかりやすくするために見た目を調整します。本当はここが楽しいのですが今回は時間の都合で端折ります。カラフルな鎖になりました。
つぎは角度を変えてみます。変数thetaに1を代入します。単位はラジアン、角度を表してます。角度を座標に反映するにはどうやったら良いでしょうか?
そう、三角関数の登場です。小学生向けの内容ですね。Xはコサイン、Yはサイン。これでthetaに応じた角度になります。これが1ラジアンの角度。
いったん全体を真ん中に移動して、今度はアニメーションやってみます。最初に言った通り、毎秒60回描画されていて、現在のフレーム数はframeCountと言う特殊な変数に格納されています。これをthetaに代入してみます。どうなるかイメージしてみてください。
こうなります。秒速60ラジアンはさすがに速すぎるので1000で割りましょう。こうなります。これで秒速6センチラジアンになります。
ここで見やすくするために一旦間引きます。
では角度にも係数をつけてみます。インデックスをかけて外ほど速く回るようにしてみます。2個目のマルは1個目の2倍、3個目のマルは1個目の3倍の速度で回転します。どうなるかイメージしてみてください。
こうなります。外ほど早く回転するのがわかります。大丈夫ですかね。ではこれがもっと多くなったときの動作。60個になったときにどうなるかというと?
こうなります。こっから、どうなるかイメージできていますか?しばらく一緒に眺めてみましょう。

2サビの「君といると」のところぐらいで最後の編集を終えていれば、曲の終わりとdemoの終わりが同じくらいに収まります。

後説

ラスサビ終わったあたりで話し始めます。

こんな感じで変数とループを使うと同じことを何回も書かなくて良くて、面白いものを作れるよという話をしました。
バックトラックはPerfumeの「ねぇ」です。
このデモの元ネタはマット・ピアソンの「ジェネラティブ・アート」という本で見たもの…と思うんですが別の本かもしれません。学生のときにめっちゃ参考にしていて、当時はprocessingっていう言語で書いていたのですが、今回はそれをもとにしたp5.jsを使って解説しました。

この本なのですが、もしかしたら書いてなかったかも。手元にないのでうろ覚えです。

https://www.amazon.co.jp/dp/4861009634

完全に曲が終わり、demoの1ループが過ぎたところで挨拶します。

Thank you for your listening!
以上、ここまでのお相手は カワリミ人形 でした またね!

感想・反省

「恐み恐みも」と「ここまでのお相手は」を発表で使いたいな〜と思っていたので実際に使えてよかったです。今後も狙っていきます。

時間の制約に悩まされた発表でした。楽曲を当て込みたいというアイデアを思いついて実行したのですが、最後に見せたい動作が2分弱かかる制約はかなりきつかったです。
準備をしっかりできるのであれば音楽も自分で作りたいですね。将来の課題です。

話しっぱなしになってしまったのも少し勿体なく感じています。CoderDojoでライブコーディングしたときはもっとゆったりと「どこを変数にまとめられるでしょうか?」「ここを変えるとどうなると思いますか?」というコールアンドレスポンスをやりながら話せたので、そちらのほうが参加者の理解度も高まったと思います。
最初に書いた台本では結構いろいろと寄り道してp5.jsの動作を説明していたのですが、とても5分に入り切らなかったのでバッサリ切りました。
とはいえ、今回は聴衆が全員skillfulなエンジニアだったので、コードの細かい解説はせずとも伝わったと思います。自動編集とジェネラティブアートの視覚的面白さで突っ走りました。

この発表のためにautoplay.vimを久しぶりに引っ張り出してきましたが、なかなか良いものを作ったなと自画自賛しました。
ただ、保存を;にマッピングしたのは失敗でしたね。どうせ画面に反映するタイミングは決まっているので、それもautoplayの台本に含めて<cr>だけ押していれば進行するようにするべきでした。

反省は多かったですが、概ね充実感のある登壇をできました。
ネタさえあればこれからもこんな感じで発表を頑張っていきたい所存です。See ya around!

Discussion