Processingで始める周期運動

こんにちは。KSWLのごつちやんです。
ここでは「三角関数を使って何かする」ことを「周期運動」と銘打って、processingを使ってやっていこうと思います。

processingのインストール

processingのインストールについては他にもたくさん記事があると思うので、そちらを参照してください。(以下のサイトなどが参考になるかも)
【Processing入門】インストールと使い方(Windows編) | アルゴリズム雑記
processing入門:Processing 2のダウンロードとインストール 梶山 喜一郎

processingの基本的なメソッド

以下に自分の記事があるので参考にしてください。この記事の中でも、「基本的な構造」「円と楕円」「三角関数」「ある範囲の変数を別の範囲に移す」の項を特に読んでおくと良いです。
processingとopenframeworksを比較してみる - gotutiyan’s blog

三角関数について

最初に周期運動を三角関数を使って表現するものと説明しました。一般に三角関数は以下のような形をしています。
f:id:gotutiyan:20190212141243p:plain
ここで見て欲しいのは、三角関数というグラフは、xがとる値を増やすに連れてその値は大きくなる、小さくなるを繰り返して動くものだということです。

でも、プログラムの中でこのxの値をどのように変えれば良いのかという問題があります。そこで、xを時間に設定するという考えのもと実装すれば、綺麗なものになります。

また、三角関数は-1~1の範囲しか取りません。この値をプログラムで利用すれば、たった1ピクセル単位でしか振動せず、変化が分かりません。このため、これから行う実例では、三角関数の値に100などの大きな数をかけることで、-100~100まで範囲を拡張して利用することになります。この点も重要なので、覚えておきましょう。

三角関数の利用

では、いくつかの例をもとにして、実際に三角関数を利用してみます。

円の直径に利用する

三角関数の値を円の直径に利用することで、円が周期的に大きくなったり小さくなったりします。
ここでは、xに時間としてframeCountを50で割ったものを利用しています。なぜ50で割るのかといったことは、こうすれば程よい速さで周期が回るので良いから、という説明をしておきます。
また、三角関数の値を動かしたい半径に対応させるため、[-1~1]の範囲を[50~450]に移します。

void setup(){
  size(500,500);
}

void draw(){
  background(255);
  float radian=frameCount/50.0;
  float r=map(sin(radian),-1,1,50,450);
  ellipse(width/2,height/2,r,r);
}
円の位置に利用する

円のx座標に三角関数を適用すれば、円が左右に動きます。
以下の例では、三角関数をx座標に利用し、先ほどの例と同様に[-1~1]の範囲を[50~450]に移します。

void setup(){
  size(500,500);
}

void draw(){
  background(255);
  float radian=frameCount/50.0;
  float x=map(sin(radian),-1,1,50,450);
  ellipse(x,height/2,50,50);
}
円の色に利用する

色をHSBで使用する時、Hの濃さを三角関数で操作すれば、虹色に変化します。
色は通常0~255で指定するので、[-1~1]の範囲を[0~255]に移します。
また、frameCount/100.0とすることで、frameCount/50.0よりも少し遅い変化を実現します。

void setup(){
  size(500,500);
}

void draw(){
  background(255);
  colorMode(HSB);
  float radian=frameCount/100.0;
  float c=map(sin(radian),-1,1,0,255);
  fill(c,255,255);
  ellipse(width/2,height/2,450,450);
}

もう少し三角関数を使う

円挙動

これまでの例では、三角関数は1つしか使われていませんでした。ここからはさらに使う量を増やして見ましょう。
x座標にcos、y座標にsinを利用してみると、円の挙動は円になります。(少し変な表現ですが笑)

今回もmap()を使っていますが、このような変換であれば200*sin(radian)と同じなので、より表記が簡単なこちらを利用することもできます。が、混乱を避けるため全てmapで行います)

void setup(){
  size(500,500);
}

void draw(){
  background(255);
  float radian=frameCount/50.0;
  float x=map(cos(radian),-1,1,-200,200);
  float y=map(sin(radian),-1,1,-200,200);
  ellipse(x+width/2,y+height/2,50,50);
}
リサージュ曲線

先ほどの円運動は、x=cos(radian)、y=sin(radian)でした。実はこの表現方法は中々応用が利いて、この式を変えるだけで全く違う顔を見せます。
今回はx=sin(3*radian)、y=sin(4*radian)としてみましょう。
4文字付け加えるだけの簡単な変更ですが、全然違う動きをします。どのような軌跡を描くのかを見たければ、background()をコメントアウトします。
void setup(){
size(500,500);
}

void draw(){
background(255);
float radian=frameCount/100.0;
float x=map(sin(3*radian),-1,1,-200,200);
float y=map(sin(4*radian),-1,1,-200,200);
ellipse(x+width/2,y+height/2,50,50);
}

ステロイド曲線

x=cos(radian)*cos(radian)*cos(radian)
y=sin(radian)*sin(radian)*sin(radian)
と変更すれば、また新しい顔を見せます。

void setup(){
  size(500,500);
}

void draw(){
  background(255);
  float radian=frameCount/50.0;
  float x=map(cos(radian)*cos(radian)*cos(radian),-1,1,-200,200);
  float y=map(sin(radian)*sin(radian)*sin(radian),-1,1,-200,200);
  ellipse(x+width/2,y+height/2,50,50);
}

円の数を増やす

さて、ここまで周期運動を1つの円を動かすことで見てきましたが、まだまだ本領発揮とはいきません。円の数を増やすことで、さらなる感動を得られます。
今からやることは、円をいくつも用意してその周期の速さを変える(radianの進む速さを変える)ことです。

このために、配列を1つ用意します。
radianの配列です。以下はリサージュ曲線を例に、円をたくさん描画するものです。

ずっと見ていると複数の円が重なる場面が出てくるところが最高ですね。
PI/512*(i+1)/50; という部分にはそれなりの経験からくるものがあるのですが、説明は割愛します。PIというのは円周率3.1415..ですね。

float radian[]=new float[50];
void setup(){
  size(500,500);
}

void draw(){
  background(255);
  for(int i=0;i<50;i++){
   radian[i]+=PI/512*(i+1)/50.0;
   float x=map(cos(3*radian[i]),-1,1,-200,200);
   float y=map(sin(4*radian[i]),-1,1,-200,200);
   ellipse(x+width/2,y+height/2,10,10);
  }
}

今までと同様に、ここでのx,yに代入する式を変えることで全く違う顔を見せます。
以下はアステロイド曲線です。

float radian[]=new float[50];
void setup(){
  size(500,500);
}

void draw(){
  background(255);
  for(int i=0;i<50;i++){
   radian[i]+=PI/512*(i+1)/20.0;
   float x=map(cos(radian[i])*cos(radian[i])*cos(radian[i]),-1,1,-200,200);
   float y=map(sin(radian[i])*sin(radian[i])*sin(radian[i]),-1,1,-200,200);
   ellipse(x+width/2,y+height/2,10,10);
  }
}

色をグラデーションさせる

今、円は全て白塗りになっていて、少し味気ないです。色を加えることでもう少し綺麗に見せることができます。
具体的には、今ある50個の円を全てグラデーションさせます。

float radian[]=new float[50];
void setup(){
  size(500,500);
  colorMode(HSB);
}

void draw(){
  background(255);
  for(int i=0;i<50;i++){
   radian[i]+=PI/512*(i+1)/20.0;
   float c=map(i,0,49,0,255);
   float x=map(sin(3*radian[i]),-1,1,-200,200);
   float y=map(sin(4*radian[i]),-1,1,-200,200);
   fill(c,255,255);
   ellipse(x+width/2,y+height/2,10,10);
  }
}

Atcoder ABC99-D GoodGrid

問題

D - Good Grid

色iを色jに塗り替える時に感じる違和感と、色が塗られたマス目の情報が与えられる。この時、(i+j)%3=(x+y)%3となるような(i,j),(x,y)のマス目は全て同じ色に塗らなければならない。感じる違和感の最小値を求める。

解説

全探索をします。
前準備として、v[0,1,2][0,1,2,,c]の配列において、「v[i][j]:=(x+y)%3==i のマス目における色j の出現回数」とすることで、(x+y)%3=0,1,2それぞれについて、最初はどの色が何個あるのかを持っておきます。

次に、i,j,kの3重ループを0〜cで回し、i≠j≠kの時、(x+y)%3=0のマス目をi, =1のマス目をj, =2のマス目をkで塗るような全探索をします。これによって最小値を調べ、出力すれば良いです。i,j,kを決めれば、あとは(x+y)%3のそれぞれについて、「今塗られている色の個数」*「その色をi,j,kに変える時の違和感」の総和をとります。

以下、コードです(#includeは省略)

int main (){
    int n,c;cin>>n>>c;
    vector<vector<int>> v(3,vector<int>(c)),cost(c,vector<int>(c));
    rep(i,0,c)rep(j,0,c)cin>>cost[i][j]; //違和感
    rep(i,0,n)rep(j,0,n){
        int x;cin>>x;
        v[((i+1)+(j+1))%3][x-1]++;
    }

    int ans=1e18;
    rep(i,0,c){ //%3==0をiに
        rep(j,0,c){  //%3==1をjに
            if(i==j)continue;
            rep(k,0,c){ //%3==2をkにするとき
                if(i==k || j==k)continue; 
                int ret=0;
                rep(ind,0,3){ //%3の値
                    int x;
                    if(ind==0)x=i;
                    else if(ind==1)x=j;
                    else if(ind==2)x=k;
                    rep(val,0,c){
                        ret+=v[ind][val]*cost[val][x];
                    }
                }
                ans=min(ans,ret);
            }
        }
    }
    cout<<ans<<endl;
}

yukicoder No.643 Two Operations No.2

問題
No.643 Two Operations No.2 - yukicoder

解説
この解説は多分悪い例として見ていただくのが良いと思います。
考察のできる人は、公式解説通りに考えられるのでしょう。でも僕にはそんな思いつきはありませんでした。ならマシンパワーに頼るしかない、ということで、全探索解でACしました。これも競技プログラミングです。

操作1,2を様々な順番で実行して行って、x==yとなるような最小手順を探してみます。
問題はこの手順をどれくらいの深さまで見るかですが、適当に手元で実行すると、30回では時間がかかりすぎたので、20回にしました。

ans=10000と初期化しておいて、x==yになればctとの最小値を取ります。最終的にans==10000であれば、x==yにならなかったので-1を出力です。
今回は偶然答えが3以下になるような問題だったので、全探索が通って嬉しいですね。

#include <iostream>

using namespace std;
int ans=100000;
void dfs(int x,int y,int ct){
    //cout<<x<<y<<endl;
    if(ct>20){ //20回操作を試して見たら終了
        return;
    }
    if(x==y){
        ans=min(ans,ct);
        return ;
    }
    dfs(y,x,ct+1);  //操作1
    dfs(x+y,x-y,ct+1); //操作2
    return;
}
 
int main (){ 
    int x,y;
    cin>>x>>y;
    dfs(x,y,0);
    if(ans!=100000)cout<<ans<<endl;
    else cout<<-1<<endl;
    return 0;
}

yukicoder No.739 大事なことなので2度言います

解説

文字列を半分に切って、同じかどうか判定します。

文字列を抽出するsubstr(開始index, そこから何文字か)を利用すると簡単にかけます。
substr()の使用例は以下のような感じです。

string s="abcde";
cout<<s.substr(1,3)<<endl; //"bcd"が出力。s[1]から3文字分取る。

よって、この問題の解答は以下のようになります。

#include <iostream>
#include <string>
using namespace std;
int main (){ 
    string s;
    cin>>s;
    if(s.substr(0,s.size()/2)==s.substr(s.size()/2,s.size()/2))cout<<"YES"<<endl;
    else cout<<"NO"<<endl;
    return 0;
}


他には、単純に1文字ずつ見ていく方法があります。

int main(){
    string s;cin>>s;
    //文字列長が奇数ならそもそもNO
    if(s.size()%2==1){
        cout<<"NO"<<endl;
        return 0;
    }
    //文字列を添字が0から、s.size()/2からの2つをスタート地点として、順番に比べる。1つでもダメならその時点でNO。
    rep(i,0,s.size()/2){
        if(s[i]!=s[i+s.size()/2]){
            cout<<"NO"<<endl;
            return 0;
        }
    }
    cout<<"YES"<<endl;
    return 0;

processingでエクスポートすると画像が出ない話

processingは、「アプリケーションとしてエクスポート」すると、.exe(Windows)や.app(Mac) などの形式で出力できます。
しかし画像ファイルを扱っていた時、単に出力しただけでは起動ファイルを実行しても画像が表示されません。

解決策

出力前にコーディングしていた時には、画像を.pdeと同じ階層に置くことで、loadImage()で読み込むことができていました。
出力の際にも同じことをします。出力されたファイルには画像ファイルの情報は含まれないので、自分で置く必要があります。

具体例(一応)

念のため、具体例を示します。

以下のコードtest.pde の中身が以下のようになってしているとします。

size(500,500);
PImage p=loadImage("1.png");
image(p,0,0);

この時、編集中のフォルダ[test]は以下のようになります。
f:id:gotutiyan:20181031185446p:plain
.pdeと同じフォルダに画像を置いています。

次に、これを出力すれば以下のような中身のフォルダができます。(画像はmac
f:id:gotutiyan:20181031185555p:plain


これらの起動ファイルを実行しても画像は表示されないので、自分で置きます。(画像はmac
f:id:gotutiyan:20181031185835p:plain

これで起動すると画像が表示されるようになりました。
windowsでも同様に画像を配置すれば良いです。

ノイズ木構造

再帰関数で二分木をどわーっと書いて、点をノイズで揺らしてみました。僕の好きなノイズと再帰でごり押しした感じの作品ですが、動的に生成される木構造が良いですね。本当はノードが増えたり減ったりしたときに、辺をニョキッと滑らかに生やしたかったんですが、やり方がよく分かりませんでした。。

コードは以下の通りです。最初の一回だけ、nowDeepが0なので、map(i,0,0,0,360)となってこれはinfinityを返すぞ!と怒られますが、気にしない気にしない。

PIC10 p10=new PIC10();
void setup(){
  size(1200,600,P2D);
  smooth();
  
}

void draw(){
  background(0);
  p10.move(width/2,10,0);
}

class PIC10 {
  int nowDeep=0; //現状の最大の再帰の深さ
  PIC10() {
  }

  void move(float x, float y, int i) {
    float noi=200*noise(100*i+frameCount/700.0);  //横の節との間隔
    float noitate=200*noise(1000*i+frameCount/700.0);  //縦の節との間隔
    if (y+noitate>=height-20) {  //画面に収まる範囲で再帰をする
      fill(map(i, 0, nowDeep, 0, 360), 100, 100);
      ellipse(x, y, 10, 10);
      nowDeep=i;  //再帰を打ち切る際にnowDeepを更新
      return;
    }

    colorMode(HSB, 400, 100, 100);
    fill(map(i, 0, nowDeep, 0, 360), 100, 100);
    ellipse(x, y, 10, 10);
    stroke(0,0,100);
    line(x, y, x+noi, y+noitate);
    line(x, y, x-noi, y+noitate);
    move(x+noi, y+noitate, i+1);
    move(x-noi, y+noitate, i+1);
  }
}

ランダムウォーク2

先日のランダムウォークを改良しました。
gotutiyan.hatenablog.com

方向ベクトルを斜めにして、軌跡を点線に。さらに円の内部を動く色付きのものと、円の外を動く白色のものに分けてみました。


白いものは画面外に出たとき、もしくは円の中に入ったときに、強制的に引き戻す方向に変えさせます。
色付きのものは、円を出ようとしたときに、強制的に引き戻す方向に変えさせます。

引き戻す方向は案外簡単に設定できて、引き戻す条件に合致した瞬間のx,y座標をX,Yとおけば、「出て行く」動きを引き戻すなら、X,Yそれぞれの反対の符号を持つdir[ ]を、「入りこむ」動きを引き戻すなら、X,Yそれぞれと同じ符号を持つ dir[ ]を足せば良いです。

また、軌跡を表す線を点線にしましたが、これは軌跡を格納している配列において、「5で割った余りが0,1,2となるような添字の時だけline()を引く」ということをしています。
つまり、3ピクセル描いて2ピクセル描かない、をひたすら繰り返します。

コードはprocessingで、以下の通りです。

RandomWalk2 random10_color[]=new RandomWalk2[100];
RandomWalk2 random10_white[]=new RandomWalk2[200];
void setup() {
  size(600,600,P2D);
  for (int i=0; i<random10_color.length; i++)random10_color[i]=new RandomWalk2(0);
  for (int i=0; i<random10_white.length; i++)random10_white[i]=new RandomWalk2(1);
}

void draw() {
  background(0);
  translate(width/2, height/2);
  noFill();
  ellipse(0, 0, 500, 500);
  for (int i=0; i<random10_color.length; i++)random10_color[i].move();
  for (int i=0; i<random10_white.length; i++)random10_white[i].move();
}

class RandomWalk2 {
  PVector now;
  PVector dir[]=new PVector[4];
  PVector diff;
  ArrayList<PVector> prev=new ArrayList<PVector>();
  int changeDir;
  float c;
  float radius=250;
  int mode=0; //0なら円内部の色付き、1なら円外部の白
  RandomWalk2(int emode) {
    mode=emode;
    if (mode==0)now=new PVector(0, 0);
    else now=new PVector(random(width)-width/2, random(height)-height/2);
    dir[0]=new PVector(1, 1);  //方向ベクトルたち
    dir[1]=new PVector(-1, -1);
    dir[2]=new PVector(1, -1);
    dir[3]=new PVector(-1, 1);
    diff=new PVector(1, 1);
    changeDir=(int)random(30, 50);
    c=random(360);
  }

  void move() {
    if (mode==0) {
      if (dist(0, 0, now.x, now.y)>radius) { //色つきのが円の外に出ない
        if (now.x<0&&now.y<0)diff=dir[0];
        if (now.x>0&&now.y>0)diff=dir[1];
        if (now.x<0&&now.y>0)diff=dir[2];
        if (now.x>0&&now.y<0)diff=dir[3];
      }
    } else {
      if (dist(0, 0, now.x, now.y)<radius) { //白いのが円の中に入らない
        if (now.x>0&&now.y>0)diff=dir[0];
        if (now.x<0&&now.y<0)diff=dir[1];
        if (now.x>0&&now.y<0)diff=dir[2];
        if (now.x<0&&now.y>0)diff=dir[3];
      }
      if (now.x<-width/2)diff=dir[0];//白が画面の外に出ない
      if (now.x>width/2)diff=dir[1];
      if (now.y<-height/2)diff=dir[3];
      if (now.y>height/2)diff=dir[2];
    }

    if (frameCount%changeDir==0) { //フレームごとでの方向変更
      int x=(int)random(0, 4);
      diff=dir[x];
    }
    prev.add(new PVector(now.x, now.y));
    while (prev.size()>100)prev.remove(0);
    now.add(diff);
    if (mode==0) {    //円の色
      colorMode(HSB,360,100,100,100);
      fill(c, 100, 100,100);
    } else {
      colorMode(RGB);
      fill(255);
    }
    noStroke();
    ellipse(now.x, now.y, 10, 10); //円の描画
    
    colorMode(HSB);
    if (mode==0) {  //線の色
      colorMode(HSB);
      stroke(c, 100, 100, 100);
    } else {
      colorMode(RGB);
      stroke(255);
    }
    for (int i=1; i<prev.size(); i++) {  //線を描く
      if (i%5==0||i%5==1||i%5==2)line(prev.get(i).x, prev.get(i).y, prev.get(i-1).x, prev.get(i-1).y);
    }
  }
};