趣味としてのクリエイティブ・コーディング:008:パズルはお好き?
プログラミングはパズルみたいなところがあります。
マッチ棒を 2本動かして正方形を 4つにしてくださいとかなんとか、そんなパズルがお好きならきっとプログラミングも好きになると思いますよ。
今回はそんなプログラミング自体の楽しさを知るプログラミング回です!
さて、前回ソースコードを整理してこのようなコードになりました。
<html>
<head>
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.7.2/p5.js"></script>
</head>
<body>
<script>
// Creative Commons CC0
// sc007 趣味としてのクリエイティブ・コーディング:007:ちょっとコードを整理!
function setup() {
createCanvas(400, 400);
colorMode(HSB, 360, 100, 100, 100);
blendMode(SCREEN);
noStroke();
noLoop();
}
function draw() {
background(0, 0, 0, 100);
translate(40.0, height / 2.0);
for (waveNo = 1; waveNo <= 3; waveNo += 1) {
for (lineNo = -2; lineNo < 5; lineNo += 0.5) {
for (radians = 0; radians < TWO_PI; radians += 0.01) {
eHue = lineNo * 60;
eSat = 100 - radians * 5;
eBri = radians * waveNo * waveNo * 3;
eX = radians * waveNo * 25;
eY = -sin(radians) * radians * lineNo * waveNo * waveNo * 2.0;
eR = lineNo;
fill(
eHue,
eSat,
eBri,
100
);
ellipse(
eX,
eY,
eR,
eR
);
}
}
}
}
</script>
</body>
</html>
コードは綺麗になったけど、肝心の結果が前と変わっちゃって、でもどうすることもできなくて、前回はそこで涙を呑んだのでした。
元々の画像はこうでした。
今回はそのリベンジ!
目的に合うプログラムを書きたいときに、どう考えるのか?の一例をお見せします。
if 文や、プログラミングのキモである処理の順番が出てきますよ。
コードの動かし方についてはシリーズ第一回目をご覧ください。
どこがダメなのか、どうありたいのか
元々の画像と大きく違うのが、一番大きい波形の x 軸方向の描かれ方です。
そこだけ抜き出してみると、
今回の一番大きい波形
元々の一番大きい波形
元々に比べて x 軸方向がキュッと縮んじゃってる感じです。
そこで x 軸の計算式を比べてみましょう。
今回のコードでは、小・中・大の 3つの波形を waveNo のループを 3回繰り返すことで描いていました。
for (waveNo = 1; waveNo <= 3; waveNo += 1) {
for (radians = 0; radians < TWO_PI; radians += 0.01) {
eX = radians * waveNo * 25;
片や元々のコードでは、3つの ellipse() を使って描いています。
for (radians = 0; radians < TWO_PI; radians += 0.01) {
eX = radians * 25;
ellipse(eX, eY * 3, eR, eR);
ellipse(eX * 2, eY * 8, eR, eR);
ellipse(eX * 4, eY * 20, eR, eR);
問題となる「大」の波形は、
・今回のコードでは waneNo = 3 のとき、
・元々のコードでは 3番目の ellipse()
で描いています。
そこを見比べてみると、今回のコードは、
eX = radians * waveNo * 25;
で、waveNo = 3 のときだから、
eX = radians * 3 * 25;
こう。
元々のコードの方は、
eX = radians * 25;
ellipse(eX * 4,
なので、
ellipse(radians * 25 * 4,
こうです。
ということは、
radians * 3 * 25; // 今回
radians * 25 * 4, // 元々
つまり、
radians * 25 * 3; // 今回
radians * 25 * 4, // 元々
で、
元々 4倍してたはずのものを、今回は 3倍にしかしてない!
ってことですね。
それで今回のほうがキュッと縮んで見えるわけです。
今回
元々
パズルを解こう!
よし!原因がわかったので、修正してみましょう。
for (waveNo = 1; waveNo <= 3; waveNo += 1) {
eX = radians * waveNo * 25;
waveNo が for ループの繰り返しの中で 1, 2, 3 と変わっていきます。
1, 2 のときはそのままでいいけど、 3 のときは 3倍じゃなくて 4倍にしたいわけですね。
こういう「何々のときはこうしたい」というのはプログラムではよくあることで、ちゃんとそれ用の仕組みがあります。
その名も「if 文」〜!
if は「もし」って意味で、今回は「もし waveNo が 3だったら 4倍したい」という意味で、
if (waveNo == 3) { // もし waveNo が 3だったら
eX = radians * 4 * 25; // 4倍する
}
こう書くことができます。
これを、x 軸の計算式の後に書き加えてみましょう。
eX = radians * waveNo * 25; // このままだと 3倍
if (waveNo == 3) { // もし waveNo が 3だったら
eX = radians * 4 * 25; // 4倍する
}
すると?
ほら! 元々の図になったでしょ?
if 文では = じゃなくて ==
if 文の中で「waveNo が 3だったら?」のところは「waveNo == 3」というように、 = じゃなくて == と書きました。
= は変数に値をセット(代入)するもの、等しいか?と比較する時は == と書きます。
これは掟です。
掟を破ると、罰せられてプログラムが思ったように動かなくなるので、鉄の掟と思って守りましょう。
if文を使って「3 のときはこうしたい」とできるってことは、「1 のとき」「2 のとき」も別々のことができるってことですよね。
ちょっと悪ノリして、「1 のときは 2倍」「2 のときは 3倍」「3 のときは 4倍」ってこともできるわけですね。
if (waveNo == 1) {
eX = radians * 2 * 25;
}
if (waveNo == 2) {
eX = radians * 3 * 25;
}
if (waveNo == 3) {
eX = radians * 4 * 25;
}
もっとも、これだったら
eX = radians * (waveNo + 1) * 25;
で良い気もしますが…
もっと楽しい方法ないの?
これでめでたく解決なのですが…
なんかパズル解いたって感じじゃなくありません?
スッキリしないというか、この答えでいいのかな?というか、「私、頭いいー!天才!!」っていう爽快感が得られないっていうか…
if 文での解決も今回の問題の解法としては十分なのですが、今回の問題「だけ」を解決する方法って感じなんですよね。
それに、こちとら趣味でやってるもんで、問題解決にも楽しさを求めたいわけです。
ということで、他のもっと華麗できらびやかでエレガンスを感じさせる解法を考えるでがんす。
今回の問題をもう一度見てみましょう。
waveNo が 1, 2, 3 と変わりながら繰り返している中で、1倍、2倍、3倍となってしまったのを、1倍、2倍、4倍としたい。
つまりは
1, 2, 3 から
1, 2, 4 を作りたいってことです。
パズルですね!
1, 2, 3
↓
1, 2, 4
これをジーッと見つめていると…
1, 2, 3
↓
x, x, x
2 + 3 = 5 で、 1 引いたら 4 になるな…
1, 2, 3
↓
x, x, 4
1 + 2 = 3 で、同じく 1 引けば 2 になるじゃん!
1, 2, 3
↓
x, 2, 4
ん?
ここは前に 0 があると考えると、
0, 1, 2, 3
↓
x, 2, 4
0 + 1 = 1 で、同じく 1 引けば! あ、0 になっちゃった。
じゃあ、前に 1 があるということにすればいいかな?
1, 1, 2, 3
↓
1, 2, 4
1 + 1 = 2 で、1 引けば 1 になる!
この考え方でいけそうですね。
まとめると、
ひとつ前の数値と今の数値を足して 1 を引く
これをコードで書いてみましょう。
プログラミングのキモ、処理の順番
問題のコードの肝心な部分だけを抜き出して書いてみると、
for (waveNo = 1; waveNo <= 3; waveNo += 1) {
eX = radians * 25 * waveNo; // これだと 1, 2, 3倍になっちゃう
eX = radians * 25 * seikai; // 正解は、 1, 2, 4倍
}
これで変数 seikai が 1, 2, 4 となればよいわけです。
これを視覚的に確認できるようにこんなコードを使ってみましょう。
<html>
<head>
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.7.2/p5.js"></script>
</head>
<body>
<script>
// Creative Commons CC0
// sc008 趣味としてのクリエイティブ・コーディング:008:パズルはお好き?
function setup() {
createCanvas(400, 400);
colorMode(HSB, 360, 100, 100, 100);
blendMode(SCREEN);
noStroke();
noLoop();
}
function draw() {
background(0, 0, 0, 100);
translate(40.0, height / 2.0);
for (waveNo = 1; waveNo <= 3; waveNo += 1) {
seikai = waveNo * 2;
fill(0, 60, 80, 100);
ellipse(waveNo * 50, -60, 50, 50);
fill(120, 60, 60, 100);
ellipse(seikai * 50, 60, 50, 50);
}
}
</script>
</body>
</html>
上の赤い円が waveNo で、左から 1, 2, 3 の位置に描画されています。
下の緑色の円は seikai で、今は waveNo の 2倍にしているので、 2, 4, 6 の位置に描画されてます。
これが、こうなるようなコードを書ければいいわけですね。
まずは、
1, 2, 3
2 + 3 = 5 で、 1 引いたら 4 になるな…
ってことは、
前の waveNo + 今の waveNo - 1
をしたいってことですね。
コードにすると、
seikai = maeNoWaveNo + waveNo - 1;
って感じです。
これで maeNoWaveNo という変数に、前の waveNo をセットすればいいんですが、どうしましょう?
waveNo はループの中で 1 づつ加算していくんだから、前の waveNo は waveNo - 1 なのでは?
確かにそうなのですが、それだと意味が違うのでマズイんです。
waveNo - 1 はあくまで waveNo から 1 を引いた値であって、たとえ結果が同じ値だったとしても、前回の waveNo という意味ではないんですね。
意味が違うので、例えばループを一個飛ばしで 2 づつ加算することにしよう、なんてなった瞬間に機能しないプログラムになってしまうんです。
なので、その意味の通り「前回の waveNo」をセットしましょう。
それは、コードにするとこうです。
for (waveNo = 1; waveNo <= 3; waveNo += 1) {
seikai = maeNoWaveNo + waveNo - 1;
fill(0, 60, 80, 100);
ellipse(waveNo * 50, -60, 50, 50);
fill(120, 60, 60, 100);
ellipse(seikai * 50, 60, 50, 50);
maeNoWaveNo = waveNo;
}
え? それただの waveNo じゃん? 今回の waveNo じゃん?
その通り!
しかし、今回の waveNo は、次回には前回の waveNo となるのです!
は…?
狐につままれてる場合ではありません。
これがプログラミングのキモ、変数を使う場所とセットする場所の関係、処理の順番です!
これがわかればもうプログラミングなんて出来たも同然!
どんなプログラムだって書けるようになります。
先程のコードで変数 maeNoWaveNo に着目して使う場所とセットする場所を見てみましょう。
maeNoWaveNo を使ってるのはここ
seikai = maeNoWaveNo + waveNo - 1;
セットはここ
maeNoWaveNo = waveNo;
ですね。
使ってるところより、セットしてるところが後になってますよね?
ということは?
セットした値が使われるのは、次のループの回になるわけです。
つまり、今回セットした waveNo が使われるのは次回。
次回から見ると今回は…?
そう! 前回!
なので、
seikai = maeNoWaveNo + waveNo - 1;
ここで maeNoWaveNo に入っている値は、前回の waveNo の値になるわけです!
素晴らしい! 天才! 私、天才!
後は、
前に 1 があると考えればいいのかな?
1, 1, 2, 3
↓
1, 2, 4
ということなので、変数 maeNoWaveNo は一番最初に 1 にしておけばいいってことですよね。
これはコードにすると、ループに入る前に 1 にするということで、
maeNoWaveNo = 1;
for (waveNo = 1; waveNo <= 3; waveNo += 1) {
で OK です。
全体はこうなります。
maeNoWaveNo = 1;
for (waveNo = 1; waveNo <= 3; waveNo += 1) {
seikai = maeNoWaveNo + waveNo - 1;
fill(0, 60, 80, 100);
ellipse(waveNo * 50, -60, 50, 50);
fill(120, 60, 60, 100);
ellipse(seikai * 50, 60, 50, 50);
maeNoWaveNo = waveNo;
}
動かしてみると?
ほらね!
waveNo は 1, 2, 3、seikai は 1, 2, 4 となったでしょ!
ここで満足しちゃダメ!ゼッタイ!
じゃあ、変数 maeNoWaveNo を使うところよりも前にセットしちゃったらどうなるんでしょう?
やってみましょう。
maeNoWaveNo = waveNo;
seikai = maeNoWaveNo + waveNo - 1;
こうすると?
こうなっちゃいます。 1, 3, 5 ですね。
まあ、これだと maeNoWaveNo は必要なくて、
seikai = waveNo * 2 - 1;
と同じことになりますしね。
セットの場所をもとに戻して、最初の maeNoWaveNo のセットを 1 じゃなくて 0 にすると?
0, 2, 4 になりました。
これは、
前に 0 があると考えると、
0, 1, 2, 3
0 + 1 = 1 で、同じく 1 引けば! あ、0 になっちゃった。
と考えたとおりですね。
こんな風にいろいろ試してみるとより理解が進むんですよ。
じゃあ次に、変数 maeNoWaveNo を使うところよりも後にセットすればいいってことは、両者の場所をあんなに離さなくても
seikai = maeNoWaveNo + waveNo - 1;
maeNoWaveNo = waveNo;
順番さえ守れば、こんな風にくっつけてもいいですよね?
こんな風に使うところとセットが近いほうがわかり易いんじゃ?
確かにそうですが、他のところでも maeNoWaveNo を使いたくなったら、その後にセットのコードを移動しないといけないですよね?
元のコードのようにループの最後でセットするようにしておけば、修正も楽だし、先のセットの後で使っちゃう間違いも少なくなるからオススメかな。
エレガンスとは?
さあこれで、if 文を使うよりもエレガンスな解法が見つかりました。
この解法を、サインカーブを描くコードに応用してみてください。
応用してみた結果のコードは最後に載せておきますね。
ん? で、どこがエレガンスなのかって?
それは、if 文での解法が『今回の問題「だけ」を解決する方法』だったのに対して、エレガンスな解法は『普遍的な規則性で解決する方法』ってとこです。
例えば、if 文での解法のコードでサインカーブをもっと増やしたくなって waveNo <= 4 としても、 waveNo が 3 のときも 4倍、 4 のときも 4倍でイマイチな絵になっちゃうわけです。
しかし、エレガンスな解法だとこのとおり!
いいでしょ!?
さらに waveNo を 1づつ加算じゃなくて、もっと細かく 0.5づつ加算してみよう!ってなったら、
ほうら!このとおり!
実にエレガンスでがんすね?
自由度が高いっていうか、応用が効くって感じなのです。
はい、今回はここまでです。
プログラミングはパズルって感じが伝わったでしょうか?
最後に今回のコードの最終形を掲載しておきます。
<html>
<head>
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.7.2/p5.js"></script>
</head>
<body>
<script>
// Creative Commons CC0
// sc008 趣味としてのクリエイティブ・コーディング:008:パズルはお好き?
function setup() {
createCanvas(400, 400);
colorMode(HSB, 360, 100, 100, 100);
blendMode(SCREEN);
noStroke();
noLoop();
}
function draw() {
background(0, 0, 0, 100);
translate(40.0, height / 2.0);
prevWaveNo = 1;
for (waveNo = 1; waveNo <= 3; waveNo += 1) {
for (lineNo = -2; lineNo < 5; lineNo += 0.5) {
for (radians = 0; radians < TWO_PI; radians += 0.01) {
eHue = lineNo * 60;
eSat = 100 - radians * 5;
eBri = radians * waveNo * waveNo * 3;
eX = radians * 25 * (prevWaveNo + waveNo - 1);
eY = -sin(radians) * radians * lineNo * waveNo * waveNo * 2.0;
eR = lineNo;
fill(
eHue,
eSat,
eBri,
100
);
ellipse(
eX,
eY,
eR,
eR
);
}
}
prevWaveNo = waveNo;
}
}
</script>
</body>
</html>
maeNoWaveNo は名前があんまりエレガンスじゃなかったので、prevWaveNo にしました。
結果はこう。
あれ!? 最近結果の絵に進捗が無いぞ!?
更新履歴
18/11/13 冒頭に今回がプログラミング回であることを追記、if 文のコード例にコメントで説明を追記
この記事が面白かったらサポートしていただけませんか? ぜんざい好きな私に、ぜんざいをお腹いっぱい食べさせてほしい。あなたのことを想いながら食べるから、ぜんざいサポートお願いね 💕