はじめに
メリークリスマス!
この記事は、Life is Tech ! Kansaiのカレンダー | Advent Calendar 2022 - Qiitaの25日目の記事になります!
本日は、@tommy1038が担当します。私は Life is Tech ! で、メンターをやっていたり、スクールのクラスマネージャーをしていたひとです。
ちなみに普段は、大阪の会社でWebエンジニアをしています。いつもそんなに記事を書かないのに、社内のアドベントカレンダーに2つ登録してしまい、この記事で3つ目になります。みんな見てくれるとうれしいなぁ👀
宣伝
だれかが宣伝していいと言っていたので、弊社記事をはっつけてみます。笑
- Ateam Group U-30のカレンダー | Advent Calendar 2022 - Qiita
- Ateam Finergy Inc.× Ateam CommerceTech Inc.× Ateam Wellness Inc.のカレンダー | Advent Calendar 2022 - Qiita
花火文字とは、、、?
「花火文字」ってみなさんご存知でしょうか?
Google先生に尋ねると、こんな感じの画像が出てきたりします。
めちゃめちゃおしゃれですよねー。こんなキラキラした青春を楽しんでみたかったなぁ
でも、こんなクリスマスで寒い時期に、花火なんてなにを言っているんだ、という感じですよね?(クリスマスに、そんな花火をあげる会社なんてn、、、おや?誰か来たようだ、、、)
ということで、今回はp5.js
を使って、花火文字を打ち上げてみたいと思います。こんな感じを想定しております。
実は、今回この花火文字を作るのに使った技術としては、テクノロジア魔法学校で得た知識で構成されています。
テクノロジア魔法学校
テクノロジア魔法学校とは、ディズニーの世界を楽しみながら、初心者でもプログラミングを学べるオンライン学習教材です。以下、公式サイトから引用します。
「テクノロジア魔法学校」は、プログラミングの入門から基礎固めに最適なオンライン型学習教材です。
HTML/CSSやJavaScript、Processingなどを使って本格的なデジタル作品を実際に制作しながら、
プログラミング全般に通じる基礎力をしっかりと身につけることができます。( 引用: https://www.technologia-schoolofmagic.jp/learning/ )
こちらの 学習内容 | ディズニー・プログラミング学習教材「テクノロジア魔法学校」 を使って紹介すると、先ほどの花火文字
を制作するためには、大きく3つの魔法を会得することで実現することができます。
その3つの魔法とは、
- 魔法の鏡
- スクールオブフィッシュ
- 花火
になります。順を追って、軽く説明していきます。
魔法の鏡
こちらの魔法では、さまざまなエフェクトで世界を映す魔法の鏡をつくろう
ということで、34のステップが用意されています。(スクショだと9つしかないですが、下にスクロールできます。)右側のgif動画で実際にどんな魔法を習得するのか、ちらっと見ることができます。簡単にいうと、動画に対してフィルター処理をかけています。それにより、色の成分を反転させたり、グレースケールにすることができます。
スクールオブフィッシュ
こちらの魔法では、マウスを追いかける動きをつくって、魚たちのダンスをアリエルに披露しよう
ということで、36ステップが用意されています。(スクショだと8つしかないですが、下にスクロールできます。)右側のgif動画では、マウスカーソル(黒い矢印)が小さく写っていて、それを魚たちが追いかけているのがわかると思います。
花火
こちらの魔法では、「塔の上のラプンツェル」の花火のシーンをメディアアートで再現しよう
ということで、20ステップが用意されています。(スクショだと、、、以下略)右側のgif動画では、水平線から花火が上がって、花が開くのを確認できますが、それをプログラミング魔法で実現することができます。ちなみに、先ほど同様にステップ数が多いと思われるかもしれませんが、数行のコードを書くだけで1ステップ完了する粒度になっているので、その部分はあまり心配しなくても大丈夫です。
それぞれ簡単に見れましたので、制作の準備をしていきましょう。p5.js
について、説明がなかったので、ここで軽く触れておきましょう。
p5.js
p5.js
は、Processing の基本原理に基づいて、グラフィックやインタラクティブな体験を作成するための JS クライアントサイド・ライブラリになります。ざっとJavaScriptが把握できれば、すぐに使い始めることができるかなと思います。
テクノロジア魔法学校でも、MediaArtやGameの分野において、p5.js
を使われたりしています。
花火文字の実装
準備
さきほどの動画がゴールとなりますので、3つの魔法を用いてどのように実現するのか考えてみましょう。
- 水平線から花火が射出される(これは、花火の魔法を参考にしながらできそう)
- 花火が文字を形づくる(どう実装しようか?)
- 花火がそれぞれ散っていく(これは、花火の魔法を参考にしながらできそう)
と考えると、鬼門となるのは 2 のところっぽいですね。さてどうしましょうか。
スクールオブフィッシュ
の魔法を使うと、マウスカーソルに向かって魚たちを誘導することができました。つまり、「特定の座標に対して、オブジェクトを向かわすことができる」ということになります。ということは今回は、文字を形づくる座標を用意してあげれば、そこに向かって花火の弾を誘導してあげればよい、と言うことになります。
2'. 花火が文字を形づくる座標に向かうようにする(ちょっと具体的になった!)
これで、実装できそうでしょうか?、、、まだ難しいですね。どうやって、その「文字を形づくる座標」を算出するのか?ですよね。
そこで、魔法の鏡
です。この魔法を使うと、動画や画像にフィルターをかけることにより、色味を調整したりすることができました。また、RGBAなどの概念も理解することができました。つまり、文字の画像を用意して、どんなサイズで画面に表示するのかを検討し、RGBAの知識を用いて座標を算出する、ということをします。少しわかりづらいと思うので、実際にやりながら紹介していきます。
魔法の鏡
の魔法で、文字の座標を算出する
下のような画像を用意します。白背景で黒文字になっているものを用意します。(魔法の鏡
の魔法を使って用意しても良いです)サイズを調整し、最終的にどんなサイズで表示するか、を検討します。
そして、この画像とキャンバスを用意して、花火の粒子が目的とする場所の座標を算出していきましょう。まず、説明のために、薄いグレーの背景を用意します。
この下地に対して、普通に画像を置いてみます。(便宜上、中央から少し上に置いてます)
この画像に対して、座標を検索します。検索するロジックとしては、左上から右下まで、ピクセルを取り出してカラーを取得する。RGBの値が(0, 0, 0)で黒になるので、黒だったらそこのポジションを記憶する、というロジックでコードを書きます。
var photoImage = loadImage('logo.png'); // ロゴの読み込み
var positions = []; // 目的地の座標を格納
// 座標検索(stepSizeは、画像によって調整します。このロゴでは、stepSize=8で調整しました)
function dot(photoImage, stepSize) {
var pixels = photoImage.pixels;
for (var y = 0; y < photoImage.height; y += stepSize) {
for (var x = 0; x < photoImage.width; x += stepSize) {
var i = (x + y * photoImage.width) * 4;
// ピクセルを書きかえる
var red = pixels[i];
var green = pixels[i + 1];
var blue = pixels[i + 2];
if (red == 0 && green == 0 && blue == 0)
{
positions.push(createVector(x, y));
}
}
}
}
これにより、positions
の中に目的地の座標が格納されます。ちゃんと値が格納されているか、ロゴの文字を形づくれているのか、を確認するために、座標に対して円を描画して確認してみましょう。
// --- デバッグ描画: 目標とする座標を表示 ---
fill(255);
for(var i = 0; i < positions.length; i++){
ellipse(positions[i]. x, positions[i].y, 1);
}
これで、座標の位置において、白い円を描くことができます。それでは実行してみましょう。
黒い点として、文字が形作られていますね。目標とする座標が手に入りました。(説明のため、stepSize
を4にしてくくっきり表示させています)
スクールオブフィッシュ
の魔法で、オブジェクトを目的の場所に向かわせる
座標を算出できたので、次は花火の粒たちを操作していきます。花火の基幹システムに関しては、次の章で説明しますが、花火の粒たちが目的の場所にいくためのロジックとしては、以下のようなものになります。
// Particleクラス(後述)の一部抜粋
var diffX = targets[0].x - this.position.x;
var diffY = targets[0].y - this.position.y;
var vec2target = createVector(diffX, diffY);
targets[0]
には、目的の座標が格納されています。(例: (40, 80))
現在のParticle(花火の粒)の位置と目的座標とのベクトルを取得しています。
目的地に向かう最中では、下記のように動かします。
// Particleクラス(後述)の一部抜粋
var k = 0.0003; // 係数
this.velocity.x += vec2target.x * k;
this.velocity.y += vec2target.y * k;
// 重力で粒の速度が変化する
this.velocity.y += 0.015;
// 速度で座標が変化する
this.position.add(this.velocity);
花火の粒に、重力による変化も加味しています。実は、前章で
(便宜上、中央から少し上に置いてます)
と記載し、ロゴ画像の文字を少し上にさせたのは、重力の分少し下がってしまうためになります。この花火の粒は、目的地に迫ると速度が遅くなっていくので、this.velocity.mag()
でベクトルの大きさを取得すると、目的地付近での挙動を制御することができます。
花火
の魔法と組み合わせる
それでは最後に、今までの準備と花火
の魔法を組み合わせます。
基幹システムとしては、花火
の魔法を使っているため、細かく説明すると、テクノロジア魔法学校のカリキュラムに抵触してしまうので少し省略しますが、下記のような構成で花火が作られています。
- Firework: 花火の本体のクラス
- explode(): 爆発のメソッド(多数のParticleを作成する)
- draw(): 表示のメソッド
- Particle: 花火の粒のクラス
- move(): 移動のメソッド(目的地付近に到達した際に、多数のLastParticleを準備する)
- draw(): 表示のメソッド
- LastParticle: 最後に出る花火の粒のクラス
- move(): 移動のメソッド
- draw(): 表示のメソッド
追記: ちょっとクラスのコードを、、、
わかりづらい点が多いため、ちょっとコードを見せるとこんな感じです。要点になる箇所だけピックアップしています。
class Firework {
constructor() {
// 水平線の中央に用意
this.position = createVector(width / 2, 750);
// 色をランダムに決める
this.color = color(random(200, 255), random(150, 255), random(100, 255));
// モードを「move」にしておく
this.mode = 'move';
}
explode() {
// モードを「explode」に変える
this.mode = 'explode';
// 花火の大きさ(爆発の強さ)をランダムで決める
var size = random(3.0, 6.0);
// positionsは目的地の座標が格納されていて、座標検索にて値が格納されている
// 花火の粒を大量に作る
for (var posID = 0; posID < positions.length; posID++) {
// 花火本体の座標と色を使って花火の粒を作り配列に入れる
particles.push(new Particle(this.position.x, this.position.y, this.color, size, posID, positions));
}
// 表示
draw() {
// 色をセット
// 小さな円を描く
}
}
class Particle {
constructor(x, y, color, size, _posID, targets) {
// 粒子ごとに固有のIDを与える
this.posID = _posID;
// 花火本体の位置で初期化
this.position = createVector(x, y);
// 飛び散る速度を初期化
//this.velocity = createVector(random(-3.0, 3.0), random(-3.0, 3.0));
// 飛び散る向きをランダムに決める
this.velocity = p5.Vector.random2D();
// 飛び散る強さをランダムに決める
this.velocity.mult(random(size));
// 花火本体の色を引きつぐ
this.color = color;
// 目的地の代入
this.targets = targets;
}
// 移動
move() {
// 空気抵抗
this.velocity.mult(0.96);
// 差分をとって目的地へ向かうベクトルを作成
var diffX = this.targets[this.posID].x - this.position.x;
var diffY = this.targets[this.posID].y - this.position.y;
var vec2target = createVector(diffX, diffY);
//! 係数
var k = 0.0003;
this.velocity.x += vec2target.x * k;
this.velocity.y += vec2target.y * k;
// 重力で粒の速度が変化する
this.velocity.y += 0.015;
// 速度が0に近づいたら
if (this.velocity.mag() < 0.05){
// 新しく小さな花火を打ち上げる
// 花火の大きさ(爆発の強さ)をランダムで決める
}
// 速度で座標が変化する
this.position.add(this.velocity);
}
// 表示
draw() {
// 粒の色をセットする
// 小さな円を描く
}
}
// 最後に出る花火の粒のクラス
class LastParticle {
constructor(x, y, color, size) {
// 花火本体の位置で初期化
this.position = createVector(x, y);
// 飛び散る向きをランダムに決める
this.velocity = p5.Vector.random2D();
// 飛び散る強さをランダムに決める
this.velocity.mult(random(size));
// 花火本体の色を引きつぐ
this.color = color;
}
// 移動
move() {
// 空気抵抗
this.velocity.mult(0.98);
// 重力で粒の速度が変化する
this.velocity.y += 0.015;
this.position.add(this.velocity);
}
// 表示
draw() {
// 粒の色をセットする
// 小さな円を描く
}
}
元々のシステムと違う箇所としては、花火が打ち上がる際、普通は1つで上がっていきますが、花火で文字をつくる仕様上、複数必要になる点です。ちなみに私は、実装を簡単にするために、打ち上げた瞬間に花を開かせて(爆発)、花火の粒たちを目的の場所に向かわせる実装を行いました。(これで花火の本体の色を1つランダムで設定すると、粒に同じ色を付与することができます。)
つまりまとめると、以下の流れになります。
- クリックイベントを取得して、花火を1つ作る
- 1つ作成した瞬間に、花火を爆発させて、花火の粒を多数作成する
- 多数の花火の粒のそれぞれが、目的の場所にむかう
- 目的の座標に近づいて、速度が0に近くなったら、
LastParticle
を出す -
LastParticle
がランダムな方向に飛び散る
これにより、先ほどの花火文字を作ることができます。(先ほどのgif動画を再掲)
おわりに
今回は、テクノロジア魔法学校を卒業した私が、得た魔法プログラミングのTipsを用いて、「花火文字」を実装してみたお話でした。プログラミングの分野に限らずですが、最終的に目指すゴールを決めて、それがどうやったら実現できるのか?を考えることが重要です。今回の例で言うと、「花火文字」がゴールに決まったけど、それを実現するためにどうしたらいいだろう、みたいな感じです。今回はたまたま、テクノロジアで得た知識を使えましたが、ゴール次第では全然違うこともあり得たと思います。そうした時でも、実現するために何があればいいのか、を因数分解しつつ、Google先生と協力しながら進めれば、きっとゴールにたどりつけるんじゃないかな、と思います。
では、本日でLife is Tech ! Kansaiのカレンダー | Advent Calendar 2022 - Qiitaは終了になります。みなさん、良いお年を。
おまけ
では最後に、アドベントカレンダーを主催なさっている、Qiitaさんへの感謝を込めて、花火文字を作ってみましょう。(ちょっとカラーを黒めに寄せています)
座標を算出して、目的座標を出すとこんな感じ。
なので、ここに向かって、花火たちを向かわせてみましょう〜
良い感じで向かわせることができました! Qiitaさん、今年もアドベントカレンダーお疲れ様でした!