yoppa.org


Touchéセンサーを使う 3 – Touché for Arduinoで「生体シンセサイザー」を作る

今回は、Touchéセンサーの活用の3回目として、音を扱ってみたいと思います。具体的にはArduinoから送られてきた情報を、Processingを用いて音に変化していきます。

Processingで音を扱う方法は、いろいろ存在します。例えば第1クォーターのSound & Software Art WorkshopでとりあげたPure DataをProcessingからコントロールして音を出す方法などが考えられます。こうした外部とのアプリケーションとの連携をする場合には、Open Sound Control(OSC)という共通のプロトコルを使用します。

しかし、今回はあくまでProcessing単体での音響合成やサウンドファイルの再生に限定して、挑戦していきたいと思います。Processingで音を扱うためのライブラリはいくつか存在しますが、現時点で最も一般的な「Minim」というライブラリを使用して音をとり扱っていきたいと思います。

Minimライブラリについて

MinimはJavaSound APIを利用したProcessingの外部ライブラリで、現在はProcessing本体のパッケージに最初から含まれています。Minimには音に関する様々な機能が用意されています。主な機能として下記のものが挙げられます。

  • サウンドの再生 ‒ WAV, AIFF, AU, SND, MP3形式のサウンドファイルを読み込んで再生
  • 録音:入力された音を、ディスクに直接、またはメモリー上のバッファーに録音可能
  • オーディオ入力:モノラル、ステレオのオーディオ入力
  • オーディオ出力:モノラル、ステレオでサウンドを出力
  • 音響合成:シンプルな音響合成のための機能
  • エフェクト:サウンドエフェクトのためのインタフェイス
  • FFT:リアルタイムにスペクトル解析が可能

Minimについての詳細は下記のプロジェクトページを参照してください。

念の為、使用しているProcessingがMinimのライブラリ群が使用できる状態か確認してみましょう。Processingのメニューから「Spetch」→「Import Library…」で表示されるメニューの中に「minim」が入っていればOKです。

image

Minimライブラリを使ってみる

最新版のProcessing(現時点ではv.2.0.1)には、Minimライブラリーのサンプルがいくつも収録されています。これらの内容を確認してみましょう。

「File」→「Examples」でサンプルのウィンドウを開いて、その中にある「Library」→「minim」を表示します。

image

サウンドファイルの再生と、波形の描画

ではまず、シンプルな例としてサウンドファイルを読み込んで、再生してみましょう。また、再生の際にサウンドファイルの波形を表示するようにしています。

// Minimサンプル1:
// サウンドファイルの再生と、波形の描画

import ddf.minim.*;

Minim minim;
AudioPlayer player;

void setup(){
  size(512, 200, P3D);

  // minimクラスをインスタンス化(初期化)、ファイルのデータを直接指定する必要があるので「this」を引数に指定
  minim = new Minim(this);

  // loadImageで画像ファイルを読み込む際の要領でサウンドファイルを読み込む
  // サウンドファイルは、スケッチ内の「data」フォルダ内に入れるのが普通だが
  // フルパスやURLで記述することも可能
  player = minim.loadFile("marcus_kellis_theme.mp3");

  // サウンドの再生
  player.play();
}

void draw(){
  background(0);
  stroke(255);

  // 波形を描画
  for(int i = 0; i < player.bufferSize() - 1; i++){
    float x1 = map( i, 0, player.bufferSize(), 0, width );
    float x2 = map( i+1, 0, player.bufferSize(), 0, width );
    line( x1, 50 + player.left.get(i)*50, x2, 50 + player.left.get(i+1)*50 );
    line( x1, 150 + player.right.get(i)*50, x2, 150 + player.right.get(i+1)*50 );
  }
}

複数の音を再生

Audio Playerをたくさん用意することで複数のサウンドファイルを再生することも可能です。この例では、バスドラムとスネアの音を読み込んで、キーボードからの入力をきっかけにしてサウンドファイルを再生しています。

// 複数のサウンドファイルを鳴らす

import ddf.minim.*;

Minim minim;
AudioSample kick;
AudioSample snare;

void setup(){
  size(512, 200, P3D);
  minim = new Minim(this);

  // バスドラムの音を読込み
  kick = minim.loadSample( "BD.mp3", // ファイル名
                            512      // バッファサイズ
                            );

  // ファイルが存在しない場合、エラーメッセージを返す
  if ( kick == null ){
   println("Didn't get kick!");
 }
  // スネアの音を読込み
  snare = minim.loadSample("SD.wav", 512);
  // ファイルが存在しない場合、エラーメッセージを返す
  if ( snare == null ){
    println("Didn't get snare!");

  }
}
void draw(){
  background(0);
  stroke(255);

  // それぞれのサウンドの波形の描画
  for (int i = 0; i < kick.bufferSize() - 1; i++) {
    float x1 = map(i, 0, kick.bufferSize(), 0, width);
    float x2 = map(i+1, 0, kick.bufferSize(), 0, width);
    line(x1, 50 - kick.mix.get(i)*50, x2, 50 - kick.mix.get(i+1)*50);
    line(x1, 150 - snare.mix.get(i)*50, x2, 150 - snare.mix.get(i+1)*50);
  }
}

// キー入力でサウンド再生
void keyPressed(){
  if ( key == 's' ){
    snare.trigger();
  }
  if ( key == 'k' ){
    kick.trigger();
  }
}

音響合成1: Sin波を合成

では次に、Sin波を生成し音として出力してみましょう。生成したSin波は、マウスの位置で音量と周波数を変更できるようにしています。

import ddf.minim.*;
import ddf.minim.signals.*;

Minim minim;
AudioOutput out;
SineWave sine;

void setup() {
  size(512, 200, P3D);
  frameRate(60);
  smooth();
  strokeWeight(2);

  minim = new Minim(this);
  out = minim.getLineOut();
  sine = new SineWave(440, 1.0, out.sampleRate());
  sine.portamento(200);
  out.addSignal(sine);
}

void draw() {
  //波形を表示
  background(0);
  stroke(255);
  //バッファーに格納されたサンプル数だけくりかえし
  for (int i = 0; i < out.bufferSize() - 1; i++) {
    // それぞれのバッファーでのX座標を探す
    float x1  =  map( i, 0, out.bufferSize(), 0, width );
    float x2  =  map( i+1, 0, out.bufferSize(), 0, width );
    // 次の値へ向けて線を描く
    line( x1, 50 + out.left.get(i)*50, x2, 50 + out.left.get(i+1)*50);
    line( x1, 150 + out.right.get(i)*50, x2, 150 + out.right.get(i+1)*50);
  }
}

void mouseMoved() {
  // 周波数をマウスのX座標で変化させる
  float freq = map(mouseX, 0, width, 20, 1000);
  sine.setFreq(freq);
  // 音量をマウスのY座標で変化させる
  float amp = map(mouseY, 0, height, 1, 0);
  sine.setAmp(amp);
}

音響合成2: FM合成

もう少し複雑な音響合成のサンプルを作成してみましょう。下記のプログラムはFM合成(Frequency Modulation Synthesis)による音響合成のサンプルです。

// FM合成

import ddf.minim.*;
import ddf.minim.ugens.*;

Minim minim;
AudioOutput out;

// オシレーター
Oscil fm;

void setup(){
  size( 512, 200, P3D );

  // Minimクラスのインスタンス化(初期化)
  minim = new Minim( this );
  // 出力先を生成
  out   = minim.getLineOut();
  // モジュレータ用のオシレーター
  Oscil wave = new Oscil( 200, 0.8, Waves.SINE );
  // キャリア用のオスレータを生成
  fm = new Oscil( 10, 2, Waves.SINE );
  // モジュレータの値の最小値を200Hzに
  fm.offset.setLastValue( 200 );
  // キャリアの周波数にモジュレータを設定( = 周波数変調)
  fm.patch( wave.frequency );
  // and patch wave to the output
  wave.patch( out );
}

// 波形を描画
void draw(){
  background( 0 );
  stroke( 255 );
  for( int i = 0; i < out.bufferSize() - 1; i++ ) {
    float x1  =  map( i, 0, out.bufferSize(), 0, width );
    float x2  =  map( i+1, 0, out.bufferSize(), 0, width );
    line( x1, 50 + out.left.get(i)*50, x2, 50 + out.left.get(i+1)*50);
    line( x1, 150 + out.right.get(i)*50, x2, 150 + out.right.get(i+1)*50);
  }  
}

// マウスの位置によってFMのパラメータを変化させる
void mouseMoved(){
  float modulateAmount = map( mouseY, 0, height, 500, 1 );
  float modulateFrequency = map( mouseX, 0, width, 0.1, 100 );

  fm.frequency.setLastValue( modulateFrequency );
  fm.amplitude.setLastValue( modulateAmount );
}

Touchéの情報をもとに、Minimで音響生成

Minim単体の機能はだいたい把握できたところで、Touché for Arduinoからの情報でMinimの音響合成をする方法について考えていきましょう。まずは、前回同様にTouchéの回路をくみたてましょう。

image

前回使用したソフトウェアもダウンロードしておきましょう。

まずはTouchéの動作を確認して問題なければ、Minimとの連携に進みましょう。早速Touchéで検出した値と、Minimでの音響合成を合体させていきましょう。

今回はまず、Touché for Arduinoからの値を受けて、記録した場所からの距離を算出する部分のみを抜き出した制作テンプレートを用意しました。ここにMinimの機能を加えていきたいと思います。

Processing_Gprah.pde

/*
 * Touche for Arduino
 * Project Template
 *
 */

float recVoltageMax;
float recTimeMax;
float voltageMax; //電圧の最大値
float timeMax; //電圧が最大値だったときの時間
float yMax = 100; //グラフのY座標最大値
float yMin = 0; //グラフのY座標最小値
float graphMargin = 20; //グラフと画面の余白

void setup() {
  //画面サイズ
  size(800, 600); 
  //ポートを設定
  PortSelected=0; 
  //シリアルポートを初期化
  SerialPortSetup();
}

void draw() {
  background(63);

  //最大値を0に初期化
  voltageMax = timeMax = 0;

  if ( DataRecieved3 ) {
    //電圧の最大値と、そのときの時間を取得
    for (int i = 0; i < Voltage3.length; i++) {
      if (voltageMax < Voltage3[i]) {
        voltageMax = Voltage3[i];
        timeMax = Time3[i];
      }
    }
    //画面に描画するために、(x, y)座標の値を画面の大きさにあわせて変換
    float x = map(timeMax, 0, 159, 0, width);
    float y = map(voltageMax, yMin, yMax, height, 0); 
    float rx = map(recTimeMax, 0, 159, 0, width);
    float ry = map(recVoltageMax, yMin, yMax, height, 0);
    float dist = dist(x, y, rx, ry);

    //現在の最大値と記録した最大値との距離を算出してテキストで表示
    fill(255);
    text("dist = "+dist, 20, 20);
  }
}

//マウスをクリック
void mouseReleased() {
  //現在の最大値を記録
  recVoltageMax = voltageMax;
  recTimeMax = timeMax;
}

TouchéとMinimの融合1 – 植物テルミン

まず初めに簡単なサンプルで、連携を確認していきたいと思います。現在のTouchéから送られてきている電圧の値でオシレーターの周波数を変化させて「植物テルミン」をつくってみましょう。記憶した時点でのTouchéセンサーと現在との距離を、Sin波の周波数に対応させてみましょう。

/*
 * Touche for Arduino
 * Touche Theremin
 *
 */

import ddf.minim.*;
import ddf.minim.signals.*;

float recVoltageMax;
float recTimeMax;
float voltageMax; //電圧の最大値
float timeMax; //電圧が最大値だったときの時間
float yMax = 100; //グラフのY座標最大値
float yMin = 0; //グラフのY座標最小値
float graphMargin = 20; //グラフと画面の余白

Minim minim;
AudioOutput out;
SineWave sine;

void setup() {
  //画面サイズ
  size(800, 600); 
  //ポートを設定
  PortSelected=0; 
  //シリアルポートを初期化
  SerialPortSetup();

  minim = new Minim(this);
  out = minim.getLineOut();
  sine = new SineWave(440, 1.0, out.sampleRate());
  sine.portamento(200);
  out.addSignal(sine);
}

void draw() {
  background(63);

  //最大値を0に初期化
  voltageMax = timeMax = 0;

  if ( DataRecieved3 ) {
    //電圧の最大値と、そのときの時間を取得
    for (int i = 0; i < Voltage3.length; i++) {
      if (voltageMax < Voltage3[i]) {
        voltageMax = Voltage3[i];
        timeMax = Time3[i];
      }
    }
    //画面に描画するために、(x, y)座標の値を画面の大きさにあわせて変換
    float x = map(timeMax, 0, 159, 0, width);
    float y = map(voltageMax, yMin, yMax, height, 0); 
    float rx = map(recTimeMax, 0, 159, 0, width);
    float ry = map(recVoltageMax, yMin, yMax, height, 0);
    float dist = dist(x, y, rx, ry);

    //現在の最大値と記録した最大値との距離を算出してテキストで表示
    fill(255);
    text("dist = "+dist, 20, 20);

    // Sin波の周波数をセンサーの値の距離に対応させる
    float freq = map(dist, 0, 1000, 20, 1000);
    sine.setFreq(freq);
  }

  //波形を表示
  background(0);
  stroke(255);
  //バッファーに格納されたサンプル数だけくりかえし
  for (int i = 0; i < out.bufferSize() - 1; i++) {
    // それぞれのバッファーでのX座標を探す
    float x1  =  map( i, 0, out.bufferSize(), 0, width );
    float x2  =  map( i+1, 0, out.bufferSize(), 0, width );
    // 次の値へ向けて線を描く
    line( x1, 50 + out.left.get(i)*50, x2, 50 + out.left.get(i+1)*50);
    line( x1, 150 + out.right.get(i)*50, x2, 150 + out.right.get(i+1)*50);
  }
}

//マウスをクリック
void mouseReleased() {
  //現在の最大値を記録
  recVoltageMax = voltageMax;
  recTimeMax = timeMax;
}

TouchéとMinimの融合2 – 植物テルミン FM版

FM合成版の植物テルミンも作ってみました。キャリアの周波数とモジュレータの音量がTouchéの状態によって変化するようにしています。

/*
 * Touche for Arduino
 * Touche FM Theremin
 *
 */

import ddf.minim.*;
import ddf.minim.ugens.*;

float recVoltageMax;
float recTimeMax;
float voltageMax; //電圧の最大値
float timeMax; //電圧が最大値だったときの時間
float yMax = 100; //グラフのY座標最大値
float yMin = 0; //グラフのY座標最小値
float graphMargin = 20; //グラフと画面の余白

Minim minim;
AudioOutput out;
Oscil fm;

void setup() {
  //画面サイズ
  size(800, 600); 
  //ポートを設定
  PortSelected=0; 
  //シリアルポートを初期化
  SerialPortSetup();

  // Minimクラスのインスタンス化(初期化)
  minim = new Minim( this );
  // 出力先を生成
  out   = minim.getLineOut();
  // モジュレータ用のオシレーター
  Oscil wave = new Oscil( 200, 0.8, Waves.SINE );
  // キャリア用のオスレータを生成
  fm = new Oscil( 10, 2, Waves.SINE );
  // モジュレータの値の最小値を100Hzに
  fm.offset.setLastValue( 100 );
  // キャリアの周波数にモジュレータを設定( = 周波数変調)
  fm.patch( wave.frequency );
  // and patch wave to the output
  wave.patch( out );
}

void draw() {
  background(63);

  //最大値を0に初期化
  voltageMax = timeMax = 0;

  if ( DataRecieved3 ) {
    //電圧の最大値と、そのときの時間を取得
    for (int i = 0; i < Voltage3.length; i++) {
      if (voltageMax < Voltage3[i]) {
        voltageMax = Voltage3[i];
        timeMax = Time3[i];
      }
    }
    //画面に描画するために、(x, y)座標の値を画面の大きさにあわせて変換
    float x = map(timeMax, 0, 159, 0, width);
    float y = map(voltageMax, yMin, yMax, height, 0); 
    float rx = map(recTimeMax, 0, 159, 0, width);
    float ry = map(recVoltageMax, yMin, yMax, height, 0);
    float dist = dist(x, y, rx, ry);

    //現在の最大値と記録した最大値との距離を算出してテキストで表示
    fill(255);
    text("dist = "+dist, 20, 20);
    float modulateAmount = map( dist, 0, 1000, 0, 1000 );
    float modulateFrequency = map( timeMax, 0, 159, 0.1, 100 );

    fm.frequency.setLastValue( modulateFrequency );
    fm.amplitude.setLastValue( modulateAmount );
  }

  //波形を表示
  background(0);
  stroke(255);
  noFill();
  //Y座標の原点を画面の中心に移動
  translate(0, height/2);
  //バッファーに格納されたサンプル数だけくりかえし
  for (int i = 0; i < out.bufferSize() - 1; i++) {
    //サンプル数から、画面の幅いっぱいに波形を表示するようにマッピング
    float x = map(i, 0, out.bufferSize(), 0, width);
    //画面の高さいっぱになるように、サンプルの値をマッピング
    float y = map(out.mix.get(i), 0, 1.0, 0, height/4);
    //値をプロット
    point(x, y);
  }
}

//マウスをクリック
void mouseReleased() {
  //現在の最大値を記録
  recVoltageMax = voltageMax;
  recTimeMax = timeMax;
}

Processingによるデータの可視化 – 時系列のデータを可視化する

今日の目標

  • 時系列のデータファイル(タブ区切りのテキスト)を読みこみ、Processingで可視化する
  • 「ビジュアライジング・データ」の4章、「時系列」のサンプルプログラムを解析
  • まずは普通にグラフ化する手法を学ぶ

Visualizing Dataのサンプルプログラムをダウンロード

  • 書籍「ビジュアライジング・データ」のサンプルプログラム一式をダウンロードする
  • 今日の内容のベースとなる、時系列のデータをプロットするプログラムもこの中に収録されている
  • ダウンロードURL http://benfry.com/writing/archives/3

シンプルなプロット表示を理解する

時系列のデータを点の連なりで表現

  • 「ch04-milkteacoffee」→「figure_01_just_points」を開く
  • まずは実行してみる
  • 点によるシンプルなプロットが表示される

  • このプロットは、dataフォルダ内にある、tab区切りのデータファイルを読み込んでいる
  • データの内容は下記のとおり
Year	Milk	Tea	Coffee
1910	32.2	9.6	21.7
1911	31.3	10.2	19.7
1912	34.4	9.6	25.5
1913	33.1	8.5	21.2
1914	31.1	8.9	21.8
1915	29	9.6	25
1916	28	9.6	27.1
... (中略) ...
2002	21.9	7.8	18.1
2003	21.6	7.5	18.5
2004	21.2	7.3	18.8

プログラムのおおまかな構造

  • プログラムの構造 – メインのプログラム(figure_01_just_points.pde)と、FloatTableクラス(FloatTable.pde)から構成
  • メインプログラム – FloatTableクラスを利用してデータを読み込み、データをプロットする点を描画
  • FloatTableクラス – 表形式のデータファイルを読み込んで、その値を利用しやすい形に変換する。また、最大値や最小値の算出などデータを扱うための様々な便利な機能を備えている

FloatTableクラス

  • FloatTableクラスは、tsv(タブ区切り)形式の表データをファイルから読み込み、データの最大値と最小値を求めたり、余計なデータを除去したりと、様々な機能を内包している
  • FloatTableクラスのプロパティ(属性)とメソッド(機能)をまとめてみる
  • プロパティ(属性)
    • rowCount – 行の数 (横)
    • columnCount – 列の数 (縦)
    • data[][] – 読み込まれたデータ data[行][列]
    • rowNames[] – 行につけられたラベル
    • columnNames[] – 列につけられたラベル
  • メソッド
    • FloatTable(String filename) – コンストラクタ。指定されたファイルからタブ区切りのデータを読み込み、取得された値をプロパティに読み込む
    • void scrubQuotes(String[] array) – データから、引用符(")を除去する
    • int getRowCount() – 行の数を数える
    • String getRowName(int rowIndex) – 指定した行の名前を返す
    • String[] getRowNames() – 全ての行の名前を返す
    • int getRowIndex(String name) – 行の名前から行番号を返す
    • int getColumnCount() – 列の数を数える
    • String getColumnName(int colndex) – 指定した列の名前を返す
    • String[] getColumnNames() – 全ての列の名前を返す
    • float getFloat(int rowIndex, int col) – 指定した行と列の値をFloat型で返す
    • boolean isValid(int row, int col) – 指定した行と列の値が存在するかどうかを調べる
    • float getColumnMin(int col) – 指定した列の最小値を求める
    • float getColumnMax(int col) – 指定した列の最大値を求める
    • float getRowMin(int col) – 指定した行の最小値を求める
    • float getRowMax(int col) – 指定した行の最大値を求める
    • float getTableMax() – 表全体の最大値を求める
  • コメントを付加したソースコードは下記のとおり
//ファイルの最初の行は、各列の名前を示したヘッダーでなければならない
//最初の列は、各行のタイトルでなくてはならない
//その他の値は、全てfloat型の値であると理解される
//例えば、getFloat(0, 0)と指定すると、最初の行の一番始めのコラムの値が返される
//ファイルは、テキスト形式、タブ区切りで書くこと
//空白の行は無視される
//空白文字は無視される


class FloatTable {
  int rowCount;
  int columnCount;
  float[][] data;
  String[] rowNames;
  String[] columnNames;
  
  //コンストラクタ
  //指定されたファイルからタブ区切りのデータを読み込み、取得された値をプロパティに読み込む
  FloatTable(String filename) {
    String[] rows = loadStrings(filename);
    
    String[] columns = split(rows[0], TAB);
    columnNames = subset(columns, 1); //一番左上のラベルは無視
    scrubQuotes(columnNames);
    columnCount = columnNames.length;

    rowNames = new String[rows.length-1];
    data = new float[rows.length-1][];

    //最初の行はヘッダーなので無視して、次の行から読みこむ
    for (int i = 1; i < rows.length; i++) {
      if (trim(rows[i]).length() == 0) {
	continue; //空白行は無視する
      }
      if (rows[i].startsWith("#")) {
	continue;  //コメント行(#のついた行)は無視する
      }

      //タブ区切りでデータを読み込む
      String[] pieces = split(rows[i], TAB);
      //引用符を除去
      scrubQuotes(pieces);
      
      //行のタイトル(最初の列)をコピー
      rowNames[rowCount] = pieces[0];
      //最初の列(タイトル)は無視して、次の列からデータをdata配列にコピー
      data[rowCount] = parseFloat(subset(pieces, 1));

      //行のカウントを1つ増加
      rowCount++;      
    }
    //必要であれば、data配列の大きさをリサイズ
    data = (float[][]) subset(data, 0, rowCount);
  }
  
  //データから引用符を除去
  void scrubQuotes(String[] array) {
    for (int i = 0; i < array.length; i++) {
      if (array[i].length() > 2) {
	if (array[i].startsWith("\"") && array[i].endsWith("\"")) {
	  array[i] = array[i].substring(1, array[i].length() - 1);
	}
      }
      //ダブルクォートをシングルクォートに
      array[i] = array[i].replaceAll(""\"", "\"");
    }
  }
  
  //行の数を数える
  int getRowCount() {
    return rowCount;
  }
  
  //指定した列番号の行の名前を返す
  String getRowName(int rowIndex) {
    return rowNames[rowIndex];
  }
  
  //全ての行の名前を返す
  String[] getRowNames() {
    return rowNames;
  }

  
  //行の名前から行番号を返す
  //見つからなかった場合は、-1を返す
  int getRowIndex(String name) {
    for (int i = 0; i < rowCount; i++) {
      if (rowNames[i].equals(name)) {
	return i;
      }
    }
    //println("No row named '" + name + "' was found");
    return -1;
  }
  
  
  //列の数を数える
  int getColumnCount() {
    return columnCount;
  }
  
  //指定した列の名前を返す
  String getColumnName(int colIndex) {
    return columnNames[colIndex];
  }
  

  //全ての列の名前を返す
  String[] getColumnNames() {
    return columnNames;
  }


  //指定した行と列の値をFloat型で返す
  float getFloat(int rowIndex, int col) {
    //異常な値を引数が指定されていた再には、例外処理をしている
    if ((rowIndex < 0) || (rowIndex >= data.length)) {
      throw new RuntimeException("There is no row " + rowIndex);
    }
    if ((col < 0) || (col >= data[rowIndex].length)) {
      throw new RuntimeException("Row " + rowIndex + " does not have a column " + col);
    }
    return data[rowIndex][col];
  }
  
  
  //指定した行と列の値が存在するかどうかを調べる
  boolean isValid(int row, int col) {
    if (row < 0) return false;
    if (row >= rowCount) return false;
    //if (col >= columnCount) return false;
    if (col >= data[row].length) return false;
    if (col < 0) return false;
    return !Float.isNaN(data[row][col]);
  }

  //指定した列の最小値を求める
  float getColumnMin(int col) {
    float m = Float.MAX_VALUE;
    for (int row = 0; row < rowCount; row++) {
      if (isValid(row, col)) {
	if (data[row][col] < m) {
	  m = data[row][col];
	}
      }
    }
    return m;
  }

  //指定した列の最大値を求める
  float getColumnMax(int col) {
    float m = -Float.MAX_VALUE;
    for (int row = 0; row < rowCount; row++) {
      if (isValid(row, col)) {
	if (data[row][col] > m) {
	  m = data[row][col];
	}
      }
    }
    return m;
  }

  //指定した行の最小値を求める
  float getRowMin(int row) {
    float m = Float.MAX_VALUE;
    for (int col = 0; col < columnCount; col++) {
      if (isValid(row, col)) {
	if (data[row][col] < m) {
	  m = data[row][col];
	}
      }
    }
    return m;
  } 

  //指定した行の最大値を求める
  float getRowMax(int row) {
    float m = -Float.MAX_VALUE;
    for (int col = 0; col < columnCount; col++) {
      if (isValid(row, col)) {
	if (data[row][col] > m) {
	  m = data[row][col];
	}
      }
    }
    return m;
  }

  //表全体の最小値を求める
  float getTableMin() {
    float m = Float.MAX_VALUE;
    for (int row = 0; row < rowCount; row++) {
      for (int col = 0; col < columnCount; col++) {
	if (isValid(row, col)) {
	  if (data[row][col] < m) {
	    m = data[row][col];
	  }
	}
      }
    }
    return m;
  }

  //表全体の最大値を求める
  float getTableMax() {
    float m = -Float.MAX_VALUE;
    for (int row = 0; row < rowCount; row++) {
      for (int col = 0; col < columnCount; col++) {
	if (isValid(row, col)) {
	  if (data[row][col] > m) {
	    m = data[row][col];
	  }
	}
      }
    }
    return m;
  }
}

メインプログラムの構造

  • メインプログラム(figure_01_just_points.pde)では、このFloatTableクラスを活用して、時系列のデータを点によってプロットしている
  • void setup()
    • 画面の初期設定
    • タブ区切りのデータファイル、"milk-tea-coffe.tsv"をデータとして、FloatTableクラスをインスタンス化 → data
    • データを読み込んだFloatTableのインスタンスdataを利用して、初期化に必要な値を取得
    • 年 – 行の名前から
    • プロットの開始年 – 行の最小値
    • プロットの終了年 – 行の最大値
    • プロットする画面の領域を計算
  • void draw()
    • プロット画面の背景を生成
    • drawDataPoints() 関数を呼び出し、読み出すコラムは0
  • void drawDataPoints()
    • データの行数を計算
    • 指定したコラムの値を取り出し
    • 画面内に納まるように、値を整える(map関数)
    • 値を整えた座標に点を描画
  • map() 関数
    • map(値, 変換前の値の最小値, 変換前の値の最大値, 変換後の値の最小値, 変換後の値の最大値)
    • 数値の範囲を別の範囲に変換する
  • 例:プロットするデータの最小値が dataMin、最大値が dataMax のときに、y座標 plotY2 から plotY1 の範囲にちょうど納まるように値 value を変換する
    • map(value, dataMin, dataMax, plotY2, plotY1);
FloatTable data; //FloatTableクラスのインスタンス
float dataMin, dataMax; //表示するデータの最大値と最小値
float plotX1, plotY1; //プロットする画面のX座標とY座標の最小値
float plotX2, plotY2; //プロットする画面のX座標とY座標の最大値
int yearMin, yearMax;  //データの年の最小値と最大値
int[] years; //年の配列

void setup() { 
  size(720, 405); 

  //読み込むデータファイルを指定して、FloatTableクラスをインスタンス化
  data = new FloatTable("milk-tea-coffee.tsv");
  //列の名前(年)を配列yearsに読み込む
  years = int(data.getRowNames()); 
  //最小の年は、years配列の先頭
  yearMin = years[0]; 
  //最大の年は、years配列の最後
  yearMax = years[years.length - 1]; 

  //データの最小値は0に設定
  dataMin = 0; 
  //全ての数値から最大値を求める
  dataMax = data.getTableMax(); 

  //プロットする画面の座標の最大値と最小値を設定
  plotX1 = 50; 
  plotX2 = width - plotX1; 
  plotY1 = 60; 
  plotY2 = height - plotY1; 

  smooth(); 
}


void draw() { 
  //画面サイズを指定
  background(224); 
  
  //プロットするエリアを白い四角形で表示
  fill(255); 
  rectMode(CORNERS); 
  noStroke(); 
  rect(plotX1, plotY1, plotX2, plotY2); 

  //最初の列(milk)のデータをプロット
  strokeWeight(5); 
  stroke(#5679C1); 
  drawDataPoints(0);
} 


//点の並びでデータを表示する関数
//引数には表示したいデータの列を入れる
void drawDataPoints(int col) { 
  //行の数(=データの数)を数える
  int rowCount = data.getRowCount(); 
  //データの数だけくりかえし
  for (int row = 0; row < rowCount; row++) { 
    //もし正しい数値だったら(データの範囲が正しければ)
    if (data.isValid(row, col)) { 
      //行と列を指定して、データの数値をとりだす
      float value = data.getFloat(row, col); 
      //プロットする画面の幅(x座標)にちょうど納まるように、値を変換
      float x = map(years[row], yearMin, yearMax, plotX1, plotX2); 
      //プロットする画面の高さ(y座標)にちょうど納まるように、値を変換
      float y = map(value, dataMin, dataMax, plotY2, plotY1); 
      //変換した値を座標にして、点を描画
      point(x, y); 
    } 
  } 
}

情報を表示

  • 現在表示しているデータの内容や数値を文字や図形で記述する
  • 表示しているデータの内容 → Milk
  • 垂直方向の目盛(消費量)
  • 垂直方向の消費量の数値
  • 水平方向の目盛(年)
  • 水平方向の年の値
  • ラベル → Gallons cosumed per caita, Year
  • キーボードで”]”を入力すると次の列のデータに変更
  • キーボードで”[“を入力すると前の列のデータに変更

FloatTable data;
float dataMin, dataMax;

float plotX1, plotY1;
float plotX2, plotY2;
float labelX, labelY;

int rowCount;
int columnCount;
int currentColumn = 0;

int yearMin, yearMax;
int[] years;

int yearInterval = 10;
int volumeInterval = 10;
int volumeIntervalMinor = 5;

PFont plotFont; 


void setup() {
  size(720, 405);
  
  data = new FloatTable("milk-tea-coffee.tsv");
  rowCount = data.getRowCount();
  columnCount = data.getColumnCount();
  
  years = int(data.getRowNames());
  yearMin = years[0];
  yearMax = years[years.length - 1];
  
  dataMin = 0;
  dataMax = ceil(data.getTableMax() / volumeInterval) * volumeInterval;

  plotX1 = 120; 
  plotX2 = width - 80;
  labelX = 50;
  plotY1 = 60;
  plotY2 = height - 70;
  labelY = height - 25;
  
  plotFont = createFont("SansSerif", 20);
  textFont(plotFont);

  smooth();
}


void draw() {
  background(224);
  
  fill(255);
  rectMode(CORNERS);
  noStroke();
  rect(plotX1, plotY1, plotX2, plotY2);

  //タイトルを表示
  drawTitle();
  //それぞれの軸のラベル(消費量/年)を表示
  drawAxisLabels();
  //横軸の値(年)を表示
  drawYearLabels();
  //縦軸の値(消費量)を表示
  drawVolumeLabels();

  stroke(#5679C1);
  strokeWeight(5);
  //指定した列(currentColumn)のデータをプロット
  drawDataPoints(currentColumn);
}

//タイトルを表示
void drawTitle() {
  fill(0);
  textSize(20);
  textAlign(LEFT);
  String title = data.getColumnName(currentColumn);
  text(title, plotX1, plotY1 - 10);
}

//それぞれの軸のラベル(消費量/年)を表示
void drawAxisLabels() {
  fill(0);
  textSize(13);
  textLeading(15);
  
  textAlign(CENTER, CENTER);
  text("Gallons\nconsumed\nper capita", labelX, (plotY1+plotY2)/2);
  textAlign(CENTER);
  text("Year", (plotX1+plotX2)/2, labelY);
}

//横軸の値(年)を表示
void drawYearLabels() {
  fill(0);
  textSize(10);
  textAlign(CENTER, TOP);
  
  // Use thin, gray lines to draw the grid
  stroke(224);
  strokeWeight(1);
  
  for (int row = 0; row < rowCount; row++) {
    if (years[row] % yearInterval == 0) {
      float x = map(years[row], yearMin, yearMax, plotX1, plotX2);
      text(years[row], x, plotY2 + 10);
      line(x, plotY1, x, plotY2);
    }
  }
}

//縦軸の値(消費量)を表示
void drawVolumeLabels() {
  fill(0);
  textSize(10);
  
  stroke(128);
  strokeWeight(1);

  for (float v = dataMin; v <= dataMax; v += volumeIntervalMinor) {
    if (v % volumeIntervalMinor == 0) {
      float y = map(v, dataMin, dataMax, plotY2, plotY1);  
      if (v % volumeInterval == 0) {
        if (v == dataMin) {
          textAlign(RIGHT);
        } else if (v == dataMax) {
          textAlign(RIGHT, TOP);
        } else {
          textAlign(RIGHT, CENTER);
        }
        text(floor(v), plotX1 - 10, y);
        line(plotX1 - 4, y, plotX1, y);
      } else {
	  //line(plotX1 - 2, y, plotX1, y);
      }
    }
  }
}

//指定した列の値を点の並びでプロット
void drawDataPoints(int col) {
  for (int row = 0; row < rowCount; row++) {
    if (data.isValid(row, col)) {
      float value = data.getFloat(row, col);
      float x = map(years[row], yearMin, yearMax, plotX1, plotX2);
      float y = map(value, dataMin, dataMax, plotY2, plotY1);
      point(x, y);
    }
  }
}

//キー入力で表示する列を切り替える
void keyPressed() {
  if (key == '[') {
    currentColumn--;
    if (currentColumn < 0) {
      currentColumn = columnCount - 1;
    }
  } else if (key == ']') {
    currentColumn++;
    if (currentColumn == columnCount) {
      currentColumn = 0;
    }
  }
}

線分で表示する

  • 点でプロットするのではなく、線でそれぞれのデータを結んでいく
  • void drawDataLine(int col) – 指定した列のデータを線分(vertex)で結んでいる

FloatTable data;
float dataMin, dataMax;

float plotX1, plotY1;
float plotX2, plotY2;
float labelX, labelY;

int rowCount;
int columnCount;
int currentColumn = 0;

int yearMin, yearMax;
int[] years;

int yearInterval = 10;
int volumeInterval = 10;

PFont plotFont; 


void setup() {
  size(720, 405);
  
  data = new FloatTable("milk-tea-coffee.tsv");
  rowCount = data.getRowCount();
  columnCount = data.getColumnCount();
  
  years = int(data.getRowNames());
  yearMin = years[0];
  yearMax = years[years.length - 1];
  
  dataMin = 0;
  dataMax = ceil(data.getTableMax() / volumeInterval) * volumeInterval;

  plotX1 = 120; 
  plotX2 = width - 80;
  labelX = 50;
  plotY1 = 60;
  plotY2 = height - 70;
  labelY = height - 25;
  
  plotFont = createFont("SansSerif", 20);
  textFont(plotFont);

  smooth();
}


void draw() {
  background(224);
  
  fill(255);
  rectMode(CORNERS);
  noStroke();
  rect(plotX1, plotY1, plotX2, plotY2);

  drawTitle();
  drawAxisLabels();
  drawYearLabels();
  drawVolumeLabels();

  strokeWeight(5);
  noFill();
  stroke(#5679C1);
  //線でデータを結ぶ
  drawDataLine(currentColumn);
}


void drawTitle() {
  fill(0);
  textSize(20);
  textAlign(LEFT);
  String title = data.getColumnName(currentColumn);
  text(title, plotX1, plotY1 - 10);
}


void drawAxisLabels() {
  fill(0);
  textSize(13);
  textLeading(15);
  
  textAlign(CENTER, CENTER);
  text("Gallons\nconsumed\nper capita", labelX, (plotY1+plotY2)/2);
  textAlign(CENTER);
  text("Year", (plotX1+plotX2)/2, labelY);
}


void drawYearLabels() {
  fill(0);
  textSize(10);
  textAlign(CENTER);
  
  stroke(224);
  strokeWeight(1);
  
  for (int row = 0; row < rowCount; row++) {
    if (years[row] % yearInterval == 0) {
      float x = map(years[row], yearMin, yearMax, plotX1, plotX2);
      text(years[row], x, plotY2 + textAscent() + 10);
      line(x, plotY1, x, plotY2);
    }
  }
}


int volumeIntervalMinor = 5;

void drawVolumeLabels() {
  fill(0);
  textSize(10);
  textAlign(RIGHT);
  
  stroke(128);
  strokeWeight(1);

  for (float v = dataMin; v <= dataMax; v += volumeIntervalMinor) {
    if (v % volumeIntervalMinor == 0) {
      float y = map(v, dataMin, dataMax, plotY2, plotY1);  
      if (v % volumeInterval == 0) {
        float textOffset = textAscent()/2;
        if (v == dataMin) {
          textOffset = 0;
        } else if (v == dataMax) {
          textOffset = textAscent();
        }
        text(floor(v), plotX1 - 10, y + textOffset);
        line(plotX1 - 4, y, plotX1, y);
      } else {
        //line(plotX1 - 2, y, plotX1, y);
      }
    }
  }
}

//線でデータを結ぶ
void drawDataLine(int col) {  
  beginShape();
  for (int row = 0; row < rowCount; row++) {
    if (data.isValid(row, col)) {
      float value = data.getFloat(row, col);
      float x = map(years[row], yearMin, yearMax, plotX1, plotX2);
      float y = map(value, dataMin, dataMax, plotY2, plotY1);      
      vertex(x, y);
    }
  }
  endShape();
}


void keyPressed() {
  if (key == '[') {
    currentColumn--;
    if (currentColumn < 0) {
      currentColumn = columnCount - 1;
    }
  } else if (key == ']') {
    currentColumn++;
    if (currentColumn == columnCount) {
      currentColumn = 0;
    }
  }
}

より自由な発想で視覚化してみる

  • この授業の趣旨は、科学的に厳密な視覚化ではない
  • より自由な発想で、データをもとに形態を生成してみる
  • バイオVJ?

応用1:円の大きさ量でを表現してみる

  • 数値の変化を円の直径にあてはめて、円の大きさの変化によって、情報を表現してみる

FloatTable data;
float dataMin, dataMax;

float plotX1, plotY1;
float plotX2, plotY2;
float labelX, labelY;

int rowCount;
int columnCount;
int currentColumn = 0;

int yearMin, yearMax;
int[] years;

int yearInterval = 10;
int volumeInterval = 10;

PFont plotFont; 

void setup() {
  size(1024, 360);
  
  data = new FloatTable("milk-tea-coffee.tsv");
  rowCount = data.getRowCount();
  columnCount = data.getColumnCount();
  
  years = int(data.getRowNames());
  yearMin = years[0];
  yearMax = years[years.length - 1];
  
  dataMin = data.getTableMin();
  dataMax = ceil(data.getTableMax() / volumeInterval) * volumeInterval;

  plotX1 = 10; 
  plotX2 = width - 10;
  labelX = 10;
  plotY1 = 40;
  plotY2 = height - 40;
  labelY = height - 40;
  
  plotFont = createFont("SansSerif", 20);
  textFont(plotFont);

  smooth();
}


void draw() {
  background(255);

  drawTitle();
  drawYearLabels();

  noStroke();
  fill(63,127,255,30);
  //円の大きさで、データを視覚化
  drawDataCircleSize(currentColumn);
}

//円の大きさでデータを視覚化する
void drawDataCircleSize(int col) {  
  for (int row = 0; row < rowCount; row++) {
    if (data.isValid(row, col)) {
      float value = data.getFloat(row, col);
      float x = map(years[row], yearMin, yearMax, plotX1, plotX2);
      float y = height/2;
      //円の直径を、取得した値をもとに計算
      float diameter = map(value, dataMin, dataMax, 0, 200);    
      //円を描く
      ellipse(x, y, diameter, diameter);
    }
  }
}

void drawTitle() {
  fill(0);
  textSize(20);
  textAlign(LEFT);
  String title = data.getColumnName(currentColumn);
  text(title, plotX1, plotY1 - 10);
}

void drawYearLabels() {
  fill(0);
  textSize(10);
  textAlign(CENTER);
  
  stroke(224);
  strokeWeight(1);
  
  for (int row = 0; row < rowCount; row++) {
    if (years[row] % yearInterval == 0) {
      float x = map(years[row], yearMin, yearMax, plotX1, plotX2);
      text(years[row], x, plotY2 + textAscent() + 10);
      line(x, plotY1, x, plotY2);
    }
  }
}

void keyPressed() {
  if (key == '[') {
    currentColumn--;
    if (currentColumn < 0) {
      currentColumn = columnCount - 1;
    }
  } else if (key == ']') {
    currentColumn++;
    if (currentColumn == columnCount) {
      currentColumn = 0;
    }
  }
}

応用2:色の透明度で量を表現してみる

  • 描画する図形の透明度によって、データを表現してみる
  • 数値が高くなるほど、透明度が下がるようにする

FloatTable data;
float dataMin, dataMax;

float plotX1, plotY1;
float plotX2, plotY2;
float labelX, labelY;

int rowCount;
int columnCount;
int currentColumn = 0;

int yearMin, yearMax;
int[] years;

int yearInterval = 10;
int volumeInterval = 10;

PFont plotFont; 


void setup() {
  size(1024, 360);
  
  data = new FloatTable("milk-tea-coffee.tsv");
  rowCount = data.getRowCount();
  columnCount = data.getColumnCount();
  
  years = int(data.getRowNames());
  yearMin = years[0];
  yearMax = years[years.length - 1];
  
  dataMin = data.getTableMin();
  dataMax = data.getTableMax();

  plotX1 = 10; 
  plotX2 = width-10;
  labelX = 20;
  plotY1 = 30;
  plotY2 = height - 30;
  labelY = height - 30;
  
  plotFont = createFont("SansSerif", 20);
  textFont(plotFont);

  smooth();
}


void draw() {
  background(0);

  drawTitle();
  drawYearLabels();

  noStroke();
  //色の濃度(alpha)でデータを視覚化する
  drawDataAlpha(currentColumn);
}

//色の濃度(alpha)でデータを視覚化する
void drawDataAlpha(int col) {
  rectMode(CENTER); 
  for (int row = 0; row < rowCount; row++) {
    if (data.isValid(row, col)) {
      float value = data.getFloat(row, col);
      float x = map(years[row], yearMin, yearMax, plotX1, plotX2);
      float y = height/2;
      //データを色の濃度と対応させている
      float a = map(value, dataMin, dataMax, 0, 255);
      //四角形の幅を算出
      float w = (plotX2-plotX1)/parseFloat(years.length)*1.1;
      //計算した濃度で色を指定
      fill(31,127,255,a);
      //四角形を描画
      rect(x, y, w, plotY2-plotY1);
    }
  }
}


void drawTitle() {
  fill(255);
  textSize(20);
  textAlign(LEFT);
  String title = data.getColumnName(currentColumn);
  text(title, plotX1, plotY1 - 10);
}

void drawYearLabels() {
  fill(255);
  textSize(10);
  textAlign(CENTER);
  
  stroke(224);
  strokeWeight(1);
  
  for (int row = 0; row < rowCount; row++) {
    if (years[row] % yearInterval == 0) {
      float x = map(years[row], yearMin, yearMax, plotX1, plotX2);
      text(years[row], x, plotY2 + textAscent() + 10);
      //line(x, plotY1, x, plotY2);
    }
  }
}

void keyPressed() {
  if (key == '[') {
    currentColumn--;
    if (currentColumn < 0) {
      currentColumn = columnCount - 1;
    }
  } else if (key == ']') {
    currentColumn++;
    if (currentColumn == columnCount) {
      currentColumn = 0;
    }
  }
}

応用3:色相で量を表現してみる

  • 透明度ではなく、色相で表現するように変更してみる
  • 低い数値 → 青(色相=240)、高い数値 → 赤(色相=360) というようにマッピングしてみる

FloatTable data;
float dataMin, dataMax;

float plotX1, plotY1;
float plotX2, plotY2;
float labelX, labelY;

int rowCount;
int columnCount;
int currentColumn = 0;

int yearMin, yearMax;
int[] years;

int yearInterval = 10;
int volumeInterval = 10;

PFont plotFont; 


void setup() {
  size(1024, 360);
  colorMode(HSB,360,100,100,100);

  data = new FloatTable("milk-tea-coffee.tsv");
  rowCount = data.getRowCount();
  columnCount = data.getColumnCount();

  years = int(data.getRowNames());
  yearMin = years[0];
  yearMax = years[years.length - 1];

  dataMin = data.getTableMin();
  dataMax = data.getTableMax();

  plotX1 = 10; 
  plotX2 = width-10;
  labelX = 20;
  plotY1 = 30;
  plotY2 = height - 30;
  labelY = height - 30;

  plotFont = createFont("SansSerif", 20);
  textFont(plotFont);

  smooth();
}


void draw() {
  background(0);

  drawTitle();
  drawYearLabels();

  noStroke();
  
  //色相でデータを視覚化
  drawDataHue(currentColumn);
}

//色相でデータを視覚化
void drawDataAlpha(int col) {
  rectMode(CENTER); 
  for (int row = 0; row < rowCount; row++) {
    if (data.isValid(row, col)) {
      float value = data.getFloat(row, col);
      float x = map(years[row], yearMin, yearMax, plotX1, plotX2);
      float y = height/2;
      //取得したデータで色相を算出
      float h = map(value, dataMin, dataMax, 240, 360);
      float w = (plotX2-plotX1)/parseFloat(years.length)*1.1;
      //算出した色相で色を設定
      fill(h,100,100,70);  
      //四角形を描画
      rect(x, y, w, plotY2-plotY1);
    }
  }
}

void drawTitle() {
  fill(255);
  textSize(20);
  textAlign(LEFT);
  String title = data.getColumnName(currentColumn);
  text(title, plotX1, plotY1 - 10);
}

void drawYearLabels() {
  fill(255);
  textSize(10);
  textAlign(CENTER);

  stroke(224);
  strokeWeight(1);

  for (int row = 0; row < rowCount; row++) {
    if (years[row] % yearInterval == 0) {
      float x = map(years[row], yearMin, yearMax, plotX1, plotX2);
      text(years[row], x, plotY2 + textAscent() + 10);
      //line(x, plotY1, x, plotY2);
    }
  }
}

void keyPressed() {
  if (key == '[') {
    currentColumn--;
    if (currentColumn < 0) {
      currentColumn = columnCount - 1;
    }
  } 
  else if (key == ']') {
    currentColumn++;
    if (currentColumn == columnCount) {
      currentColumn = 0;
    }
  }
}

応用4:放射状に配置してみる

  • 値の変化の様子を放射状に配置してみる

FloatTable data;
float dataMin, dataMax;

float plotX1, plotY1;
float plotX2, plotY2;
float labelX, labelY;

int rowCount;
int columnCount;
int currentColumn = 0;

int yearMin, yearMax;
int[] years;

int yearInterval = 10;
int volumeInterval = 10;

PFont plotFont; 


void setup() {
  size(640, 640);
  colorMode(HSB,360,100,100,100);

  data = new FloatTable("milk-tea-coffee.tsv");
  rowCount = data.getRowCount();
  columnCount = data.getColumnCount();

  years = int(data.getRowNames());
  yearMin = years[0];
  yearMax = years[years.length - 1];

  dataMin = 0;
  dataMax = data.getTableMax();

  plotX1 = 10; 
  plotX2 = width-10;
  labelX = 20;
  plotY1 = 30;
  plotY2 = height - 30;
  labelY = height - 30;

  plotFont = createFont("SansSerif", 20);
  textFont(plotFont);

  smooth();
}


void draw() {
  background(0);

  drawTitle();
  stroke(63,127,255,180);
  translate(width/2, height/2);
  //放射状に情報を表示
  drawCirclePlot(currentColumn);
}

//放射状に情報を表示
void drawCirclePlot(int col){
  //線の太さを設定
  strokeWeight(6);
  //線の終端の形状を四角に
  strokeCap(SQUARE);
  //年を表示するテキストのサイズ
  textSize(8);
  int rowCount = data.getRowCount(); 
  for (int row = 0; row < rowCount; row++) { 
    if (data.isValid(row, col)) { 
      float value = data.getFloat(row, col);
      //全ての情報を表示した際にちょうど一周するように角度を算出
      //360° = 2π(ラジアン)
      float rot = map(years[row], yearMin, yearMax+1, 0.0, 2.0*PI);
      float x = 0; 
      float y = map(value, dataMin, dataMax, 0, width*0.6);
      //数値によって色相を変更する
      float h = map(value, dataMin, dataMax, 240, 360);
      stroke(h,100,100,50);  
      pushMatrix();
      //算出した角度だけ回転
      rotate(rot);
      //情報の量に応じて、線の長さを変える
      line(0, 0, 0, -y);
      //10年ごとに年を表示
      if(int(years[row])%10 == 0){
	text(years[row], -10, -y-8);
      }
      popMatrix();
    } 
  } 
}

void drawTitle() {
  fill(255);
  textSize(20);
  textAlign(LEFT);
  String title = data.getColumnName(currentColumn);
  text(title, plotX1, plotY1 - 10);
}

void keyPressed() {
  if (key == '[') {
    currentColumn--;
    if (currentColumn < 0) {
      currentColumn = columnCount - 1;
    }
  } 
  else if (key == ']') {
    currentColumn++;
    if (currentColumn == columnCount) {
      currentColumn = 0;
    }
  }
}

応用5:アニメーションで変化を表現する

  • 値が変化する様子を、円の大きさがアニメーションすることで表現してみる

FloatTable data;
float dataMin, dataMax;

float plotX1, plotY1;
float plotX2, plotY2;
float labelX, labelY;

int rowCount;
int columnCount;

int yearMin, yearMax;
int[] years;

int yearInterval = 10;
int volumeInterval = 10;

int currentRow = 0;

PFont plotFont; 

void setup() {
  size(640, 280);
  colorMode(HSB,360,100,100,100);
  frameRate(10);

  data = new FloatTable("milk-tea-coffee.tsv");
  rowCount = data.getRowCount();
  columnCount = data.getColumnCount();

  years = int(data.getRowNames());
  yearMin = years[0];
  yearMax = years[years.length - 1];

  dataMin = 0;
  dataMax = data.getTableMax();

  plotX1 = 10; 
  plotX2 = width-10;
  labelX = 20;
  plotY1 = 30;
  plotY2 = height - 30;
  labelY = height - 30;

  plotFont = createFont("SansSerif", 20);
  textFont(plotFont);

  smooth();
}


void draw() {
  background(0);
  noStroke();

  //項目名を表示
  drawTitles();
  //現在の年を表示
  drawCurrentYear();
  //値の変化をアニメーションで表現
  animateEllipsePlot();
}

//値の変化をアニメーションで表現
void animateEllipsePlot(){
  //それぞれの列の値を取得
  float value0 = data.getFloat(currentRow, 0);
  float value1 = data.getFloat(currentRow, 1);
  float value2 = data.getFloat(currentRow, 2);
  
  //フォントの設定
  textSize(10);
  textAlign(CENTER); 
  
  //1つ目の円
  float r0 = map(value0, dataMin, dataMax, 0, width/3);
  fill(0,70,70,70);
  ellipse(width/6,height/2,r0,r0);
  //円の中心に値を表示
  fill(0,0,100,100);
  text(r0,width/6,height/2+5);
  
  //2つ目の円
  float r1 = map(value1, dataMin, dataMax, 0, width/3);
  fill(240,70,70,70);
  ellipse(width/2,height/2,r1,r1);
  //円の中心に値を表示
  fill(0,0,100,100);
  text(r1,width/2,height/2+5);
  
  //3つ目の円
  float r2 = map(value2, dataMin, dataMax, 0, width/3);
  fill(120,70,70,70);
  ellipse(width/6*5,height/2,r2,r2);
  //円の中心に値を表示
  fill(0,0,100,100);
  text(r2,width/6*5,height/2+5);

  //年を1つ進める
  currentRow++;
  //もし最後の年まできたら、最初から繰り返す
  if(currentRow > years.length-1){
    currentRow = 0;
  }    
}

void drawTitles(){
  textSize(12);
  textAlign(CENTER);  
  fill(0,0,100,100);
  
  String title0 = data.getColumnName(0);
  text(title0, width/6, height-20);
  
  String title1 = data.getColumnName(1);
  text(title1, width/2, height-20);
  
  String title2 = data.getColumnName(2);
  text(title2, width/6*5, height-20);
}

void drawCurrentYear(){
  textSize(20);
  textAlign(LEFT);  
  fill(0,0,100,100);
  
  int currentYear = years[currentRow];
  text(currentYear, 10,24);
}

サンプルファイルのダウンロード

今回の授業の全てのサンプルのソースコードは、下記のリンクよりダウンロードしてください。


Tumblrを使う3 カスタムHTMLテーマの作成

今回も引き続き、Tumblrのテーマのカスタマイズについて解説していきます。今回は、前回のテーマのカスタマイズよりもさらに高度な方法として、Thumblrの「edit HTML」の機能を使用して、自分独自のテーマをまっさらな状態から作成する方法について取りあげていきます。

スライド資料

スライド資料は下記のリンクから閲覧してください。


Touchéセンサー解析 – Touché for Arduinoの値をつかってProcessingで表現する

前回はTouché for Arduinoセンサーをつかって、様々なジェスチャーを検出することができました。今回はTouché for ArduinoのセンサーからProcessingにどのような値が送られてきているのか、プログラムを解析しながら明らかにしていきます。また、その値をどのように利用したら、Processingで面白い表現ができるのか考えていきましょう。

サンプルプログラムのダウンロード

今回とりあげたサンプルは下記のリンクからダウンロードしてください。

前回までの復習: Touché for Arduinoの作成

まずは、前回と同様、Touché for Arduinoの回路をブレッドボードに組み立てましょう。

image

ソフトウェアは前回と同様下記のGithubアカウントからダウンロードして使用します。

前回はこのプログラムの中で、主にジェスチャーを判別する部分について解説しました。ポイントは、検出したグラフで電圧が最大のピークになっている部分の(x, y)座標を記録して、新たなジェスチャーと記録したジェスチャーの距離を算出しているという部分です。以下のコメント入りのソースで再度確認しましょう。

Processing側 – Processing_graph.pde

/*
 * Processing_graph.pde
 *
 */

// グラフ描画のクラスのインスタンス化(初期化)
Graph MyArduinoGraph = new Graph(150, 80, 500, 300, color (200, 20, 20));
// 4つのジェスチャーを記録する配列
float[][] gesturePoints = new float[4][2];
// 現在のジェスチャーと登録したジェスチャーとの距離
float[] gestureDist = new float[4];
// それぞれのジェスチャーの名前の文字列
String[] names = {
  "Nothing", "Touch", "Grab", "In water"
};

void setup() {
  //画面サイズ
  size(1000, 500); 
  // グラフのラベル設定
  MyArduinoGraph.xLabel="Readnumber";
  MyArduinoGraph.yLabel="Amp";
  MyArduinoGraph.Title=" Graph";  
  noLoop();

  //ポートを設定
  PortSelected=4; 

  //シリアルポートを初期化
  SerialPortSetup();
}


void draw() {
  background(255);

  /* ====================================================================
   グラフを描画
   ====================================================================  */

   if ( DataRecieved3 ) {
    pushMatrix();
    pushStyle();
    MyArduinoGraph.yMax=100;      
    MyArduinoGraph.yMin=-10;      
    MyArduinoGraph.xMax=int (max(Time3));
    MyArduinoGraph.DrawAxis();  
    MyArduinoGraph.smoothLine(Time3, Voltage3);
    popStyle();
    popMatrix();

    float gestureOneDiff =0;
    float gestureTwoDiff =0;
    float gestureThreeDiff =0;

    /* ====================================================================
     ジェスチャーを比較
     ====================================================================  */
     float totalDist = 0;
     int currentMax = 0;
     float currentMaxValue = -1;
    for (int i = 0; i < 4;i++) { //4つの登録したジェスチャーを比較
      //ボタンをマウスでクリックしたときには、現在のジェスチャーを配列に記録
      if (mousePressed && mouseX > 750 && mouseX<800 && mouseY > 100*(i+1) && mouseY < 100*(i+1) + 50) {
        fill(255, 0, 0);
        gesturePoints[i][0] = Time3[MyArduinoGraph.maxI];
        gesturePoints[i][1] = Voltage3[MyArduinoGraph.maxI];
      } 
      else {
        fill(255, 255, 255);
      }
      //それぞれの点と現在の状態の距離を算出
      gestureDist[i] = dist(Time3[MyArduinoGraph.maxI], Voltage3[MyArduinoGraph.maxI], gesturePoints[i][0], gesturePoints[i][1]);
      //距離の合計を算出
      totalDist = totalDist + gestureDist[i];
      //最大値を算出
      if (gestureDist[i] < currentMaxValue || i == 0) {
        currentMax = i;
        currentMaxValue =  gestureDist[i];
      }
    }
    totalDist=totalDist /3;
    // 現在のジェスチャーと登録したジェスチャーの距離から、ボタンの色を描画
    for (int i = 0; i < 4;i++) {
      float currentAmmount = 0;
      currentAmmount = 1-gestureDist[i]/totalDist;
      if (currentMax == i) {
        fill(currentAmmount*255.0f, 0, 0);
      } 
      else {
        fill(255, 255, 255);
      }
      stroke(0, 0, 0);
      rect(750, 100 * (i+1), 50, 50);
      fill(0, 0, 0);
      textSize(30);
      text(names[i], 810, 100 * (i+1)+25);

      fill(255, 0, 0);
    }
  }
}

void stop() {
  myPort.stop();
  super.stop();
}

今回はもう少しこのプログラムについて、突っ込んで内容を解析していきます。それによって、オリジナルなビジュアライズや音響合成をする際のヒントにしていきたいと思います。

まず、ArduinoからProcessingへ、何のデータがどのような形式でデータが送出されているのか解析してみましょう。まず送信側のArduinoからみていきましょう。実際に値を送信している部分、”SendData.ino” の中身をみていきます。

Arduino側 – SendData.ino

byte yMSB=0, yLSB=0, xMSB=0, xLSB=0, zeroByte=128, Checksum=0;

void SendData(int Command, unsigned int yValue,unsigned int xValue){
    yLSB=lowByte(yValue);   //yの値(16bit)の後半8bit
    yMSB=highByte(yValue);  //yの値(16bit)の前半8bit
    xLSB=lowByte(xValue);   //xの値(16bit)の後半8bit
    xMSB=highByte(xValue);  //xの値(16bit)の前半8bit

    //空白(0Byte)の値がある場所を記録
    zeroByte = 128;                         // 10000000
    if(yLSB==0){ yLSB=1; zeroByte=zeroByte+1;} // 1bit目を1に(10000001)
    if(yMSB==0){ yMSB=1; zeroByte=zeroByte+2;} // 2bit目を1に(10000010)
    if(xLSB==0){ xLSB=1; zeroByte=zeroByte+4;} // 3bit目を1に(10000100)
    if(xMSB==0){ xMSB=1; zeroByte=zeroByte+8;} // 4bit目を1に(10001000)

    Checksum = (Command + yMSB + yLSB + xMSB + xLSB + zeroByte)%255;

    if( Checksum !=0 ){
        Serial.write(byte(0));          // 先頭のビット
        Serial.write(byte(Command));      // どのグラフを描画するのかを指定するコマンド

        Serial.write(byte(yMSB));        // Yの値の前半8bit(1Byte)を送信
        Serial.write(byte(yLSB));        // Yの値の後半8bit(1Byte)を送信
        Serial.write(byte(xMSB));        // Xの値の前半8bit(1Byte)を送信
        Serial.write(byte(xLSB));        // Xの値の後半8bit(1Byte)を送信

        Serial.write(byte(zeroByte));    // どの値に0があるのかを送信
        Serial.write(byte(Checksum));    // チェック用バイト
    }
}

void PlottArray(unsigned int Cmd,float Array1[],float Array2[]){
    SendData(Cmd+1, 1,1);
    delay(1);
    for(int x=0;  x < sizeOfArray;  x++){
        SendData(Cmd, round(Array1[x]),round(Array2[x]));
    }
    SendData(Cmd+2, 1,1);
}

このソースコードの中で、実際にシリアル通信で送出するメッセージを送っている部分は、中盤にある”Serial.write()”関数が並んでいる部分です。

Serial.write(byte(0));          // 先頭のビット
Serial.write(byte(Command));      // どのグラフを描画するのかを指定するコマンド

Serial.write(byte(yMSB));        // Yの値の前半8bit(1Byte)を送信
Serial.write(byte(yLSB));        // Yの値の後半8bit(1Byte)を送信
Serial.write(byte(xMSB));        // Xの値の前半8bit(1Byte)を送信
Serial.write(byte(xLSB));        // Xの値の後半8bit(1Byte)を送信

Serial.write(byte(zeroByte));    // どの値に0があるのかを送信
Serial.write(byte(Checksum));    // チェック用バイト

ここで「Yの値」となっているのがTouchéで検出された電圧、「Xの値」となっているのがその値を検出した時間に相当します。このメッセージがひと塊となってProcessingに送られていきます。図示すると以下のようなイメージになるでしょう。それぞれのブロックが、8bit(00000000〜11111111)つまり1Byteの値を格納しています。

image

ここで気になるのが、何故、電圧(Yの値)と時間(Xの値)をそれぞれ前半・後半の2つの値に分割しているのか、という部分ではないでしょうか。

実はこれは Serial.wirte() の関数で書き出せるデータの大きさに由来します。Seria.write() では一度に1Byte(= 8bit)の値までしか送出できないという制限があります。これを10進数数値に換算すると 0〜255 の256段階ということを意味します。

しかし今回のTouchéはより繊細な電圧の変化とそれに対応する時間の変化を送りたいので、256段階では目盛が粗すぎます。そこで、送信するデータを前半8bit(11111111)と後半8bit(11111111)に分割して送信し、それを受信するProcessing側で再度合成するという手法をとっています。これによって使用できる値は16bit(0000000000000000〜1111111111111111)、10進数に直すと0〜1023の1024段階の数値を表現できるのです。

データを分割してArduinoからProcessingへ送信する様子をイメージにすると以下のようになるでしょう。

image

また、この連携プログラムでは、一連のメッセージの2番目のバイトを「コマンド」と呼んでいて、データ送信の開始から終了までを検知できるようにしています。コマンドは以下のように定義されています。

  • コマンド 1: データを送信
  • コマンド 2: 送信開始(初期化)
  • コマンド 3: 送信終了

このコマンドによって条件分岐させて、Processing側で正しくデータを受信できるようにしています。

では次にこのデータを受信するProcessing側のプログラムをみてみましょう。Processing側でシリアル通信でデータを受信するパートは、”SerialLink.pde”が相当します。

Processing側 – SerialLink.pde

import processing.serial.*;
int SerialPortNumber=2;
int PortSelected=2;

//グローバル変数
int xValue, yValue, Command;
boolean Error=true;
boolean UpdateGraph=true;
int lineGraph;
int ErrorCounter=0;
int TotalRecieved=0;

//ローカル変数
boolean DataRecieved1=false, DataRecieved2=false, DataRecieved3=false;

float[] DynamicArrayTime1, DynamicArrayTime2, DynamicArrayTime3;
float[] Time1, Time2, Time3;
float[] Voltage1, Voltage2, Voltage3;
float[] current;
float[] DynamicArray1, DynamicArray2, DynamicArray3;

float[] PowerArray= new float[0]; // Dynamic arrays that will use the append()
float[] DynamicArrayPower = new float[0]; // function to add values
float[] DynamicArrayTime= new float[0];

String portName;
String[] ArrayOfPorts=new String[SerialPortNumber];

boolean DataRecieved=false, Data1Recieved=false, Data2Recieved=false;
int incrament=0;

int NumOfSerialBytes=8; // The size of the buffer array
int[] serialInArray = new int[NumOfSerialBytes]; // Buffer array
int serialCount = 0; // A count of how many bytes received
int xMSB, xLSB, yMSB, yLSB; // Bytes of data

Serial myPort; // The serial port object

//========================================================================
// シリアル通信の初期化関数
// スピードを115200bpsで接続し、シリアルポートのバッファをクリアした後、
// シリアルのバッファ値を20Byteに設定
//========================================================================
void SerialPortSetup() {
  //text(Serial.list().length,200,200);
  portName= Serial.list()[PortSelected];
  //println( Serial.list());
  ArrayOfPorts=Serial.list();
  println(ArrayOfPorts);
  myPort = new Serial(this, portName, 115200);
  delay(50);
  myPort.clear();
  myPort.buffer(20);
}

//========================================================================
// シリアルイベント:
// シリアルポートから何か情報を受けとると呼びだされる
//========================================================================
void serialEvent(Serial myPort) {
  while (myPort.available ()>0) {
    // シリアルのバッファーから次のバイト列を読込み
    int inByte = myPort.read();
    if (inByte==0) {
      serialCount=0;
    }
    if (inByte>255) {
      println(" inByte = "+inByte);
      exit();
    }
    // シリアルポートから取得された最新のバイトを、配列に追加
    serialInArray[serialCount] = inByte;
    serialCount++;

    Error=true;
    if (serialCount >= NumOfSerialBytes ) {
      serialCount = 0;
      TotalRecieved++;
      int Checksum=0;

      for (int x=0; x<serialInArray.length-1; x++) {
        Checksum=Checksum+serialInArray[x];
      }

      Checksum=Checksum%255;

      if (Checksum==serialInArray[serialInArray.length-1]) {
        Error = false;
        DataRecieved=true;
      } 
      else {
        Error = true;
        DataRecieved=false;
        ErrorCounter++;
        println("Error:  "+ ErrorCounter +" / "+ TotalRecieved+" : "+float(ErrorCounter/TotalRecieved)*100+"%");
      }
    }

    if (!Error) {
      int zeroByte = serialInArray[6];
      // println (zeroByte & 2);
      xLSB = serialInArray[3];
      if ( (zeroByte & 1) == 1) xLSB=0;
      xMSB = serialInArray[2];
      if ( (zeroByte & 2) == 2) xMSB=0;
      yLSB = serialInArray[5];
      if ( (zeroByte & 4) == 4) yLSB=0;
      yMSB = serialInArray[4];
      if ( (zeroByte & 8) == 8) yMSB=0;

      //データ内容確認用
      //println( "0\tCommand\tyMSB\tyLSB\txMSB\txLSB\tzeroByte\tsChecksum");
      //println(serialInArray[0]+"\t"+Command +"\t"+ yMSB +"\t"+ yLSB +"\t"+ xMSB +"\t"+ xLSB+"\t" +zeroByte+"\t"+ serialInArray[7]);

      //========================================================================
      // バイト(8bit)単位に分割されたデータを合成して、16bit(0〜1024)の値を復元する
      //========================================================================
      Command  = serialInArray[1];
      xValue   = xMSB << 8 | xLSB; // xMSBとxLSBの値から、xValueを合成
      yValue   = yMSB << 8 | yLSB; // yMSBとyLSBの値から、yValueを合成

      //データ内容確認用
      //println(Command+ "  "+xValue+"  "+ yValue+" " );

      //========================================================================
      // Command, xValue, yValueの3つの値がArduinoから取得完了
      // 様々なケースごとに、グラフ描画用の配列に格納していく
      //========================================================================

      switch(Command) { //Commandの値によって、動作をふりわけ

        //配列1と配列2がArduinoから受信された状態、グラフを更新する

      case 1: // 配列にデータを追加
        DynamicArrayTime3=append( DynamicArrayTime3, (xValue) ); //xValueを時間の配列に1つ追加
        DynamicArray3=append( DynamicArray3, (yValue) ); //yValueをの電圧の配列に1つ追加
        break;

      case 2: // 想定外のサイズの配列を受けとった場合、配列を空に初期化
        DynamicArrayTime3= new float[0];
        DynamicArray3= new float[0];
        break;

      case 3:  // 配列の受信完了、値を追加した配列をグラフ描画用の配列にコピーして、グラフを描画する
        Time3=DynamicArrayTime3;
        Voltage3=DynamicArray3;
        DataRecieved3=true;
        break;
      }
    }
  }
  redraw();
}

このProcessingのコードの中で、先程8bitずつ分割したX(時間)とY(電圧)の時間を合成しているのは、下記の部分です。

xValue   = xMSB << 8 | xLSB; // xMSBとxLSBの値から、xValueを合成
yValue   = yMSB << 8 | yLSB; // yMSBとyLSBの値から、yValueを合成

合成された値は、xValueとyValueに格納されているのがわかります。この値は、データの受信中(コマンド1の際には)以下の命令でそれぞれ、DynamicArrayTime3とDynamicArray3という配列に追加されています。

DynamicArrayTime3=append( DynamicArrayTime3, (xValue) ); //xValueを時間の配列に1つ追加
DynamicArray3=append( DynamicArray3, (yValue) ); //yValueをの電圧の配列に1つ追加

最終的にデータの送信が完了(コマンド3)したら、格納した配列を別の描画用の配列にコピーしています。

Time3=DynamicArrayTime3;
Voltage3=DynamicArray3;

つまり、最終的にビジュアライズや何かしら表現する際に参照すべきデータはそれぞれ以下の配列ということになります。

  • 時間: Time3
  • 電圧: Voltage3

では、以下のパートで、この値を活用してみましょう。

Processingでビジュアライズ 1 – テキストで出力

では、さっそく取得したTime3とVoltage3を利用して、Processingで視覚的に表現してみましょう。Processingのパッケージの中にある「GraphClass.pde」と「SerialLink.pde」はそのまま使用します。メインのプログラム「Processing_graph.pde」のみ改造していきます。

ジェスチャーの特徴は、電圧(Voltage3)のピーク(最大値)の値とそのときの時間(Time3)の値の組がそのジェスチャーの特徴をあらわしています。まずは単純に電圧の最大値と、最大値が記録されたときの時間を文字でモニターしてみましょう。以下のようなシンプルなプログラムで確認可能です。

Processing側 – Processing_graph.pde

/*
 * Touche for Arduino
 * Vidualization Example 00
 *
 */

float voltageMax; //電圧の最大値
float timeMax; //電圧が最大値だったときの時間

void setup() {
  //画面サイズ
  size(800, 600); 
  noLoop();
  //ポートを設定
  PortSelected=5; 
  //シリアルポートを初期化
  SerialPortSetup();
}

void draw() {
  background(63);
  fill(255);

  //最大値を0に初期化
  voltageMax = timeMax = 0;

  if ( DataRecieved3 ) {
    //電圧の最大値と、そのときの時間を取得
    for (int i = 0; i < Voltage3.length; i++) {
      if (voltageMax < Voltage3[i]) {
        voltageMax = Voltage3[i];
        timeMax = Time3[i];
      }
    }

    //時間と電圧の範囲(最小値と最大値)を表示
    text("Time range: " +  min(Time3) + " - " + max(Time3), 20, 20);
    text("Voltage range: " +  min(Voltage3) + " - " + max(Voltage3), 20, 40);

    //電圧の最大値と、その時の時間を表示
    text("Time: " + timeMax, 20, 80);
    text("Voltage: " + voltageMax, 20, 100);
  }
}

void stop() {
  myPort.stop();
  super.stop();
}

このプログラムを実行すると、以下のように時間と電圧の範囲と、電圧の最大値とそのときの時間が表示されます。

image

Processingでビジュアライズ 2 – ピークの位置を座標で表示

では次に電圧のピークの際の時間をX座標に、電圧をY座標にして画面に表示してみましょう。ちょうど画面いっぱいに表示されるようにmap()関数をつかって値を変化するところがポイントです。

Processing側 – Processing_graph.pde

/*
 * Touche for Arduino
 * Vidualization Example 02
 *
 */

float voltageMax; //電圧の最大値
float timeMax; //電圧が最大値だったときの時間
float yMax = 100; //グラフのY座標最大値
float yMin = 0; //グラフのY座標最小値
float graphMargin = 20; //グラフと画面の余白

void setup() {
  //画面サイズ
  size(800, 600); 
  noLoop();
  //ポートを設定
  PortSelected=5; 
  //シリアルポートを初期化
  SerialPortSetup();
}

void draw() {
  background(63);

  //最大値を0に初期化
  voltageMax = timeMax = 0;

  if ( DataRecieved3 ) {
    //電圧の最大値と、そのときの時間を取得
    for (int i = 0; i < Voltage3.length; i++) {
      if (voltageMax < Voltage3[i]) {
        voltageMax = Voltage3[i];
        timeMax = Time3[i];
      }
    }
    //画面に描画するために、(x, y)座標の値を画面の大きさにあわせて変換
    float x = map(timeMax, 0, 159, graphMargin, width-graphMargin);
    float y = map(voltageMax, yMin, yMax, height-graphMargin, graphMargin); 

    //枠線を描く
    noFill();
    stroke(127);
    rect(graphMargin, graphMargin, width - graphMargin * 2, height - graphMargin * 2);

    //現在の最大値の座標を交差する線で描く
    line(x, graphMargin, x, height-graphMargin);
    line(graphMargin, y, width-graphMargin, y);

    //現在のそれぞれの最大値を文字で表示
    fill(#3399ff);
    noStroke();
    text(timeMax, x+2, height-graphMargin-2);
    text(voltageMax, graphMargin, y-10);

    //現在の最大値を円で描く
    ellipse(x, y, 20, 20);
  }
}

void stop() {
  myPort.stop();
  super.stop();
}

このプログラムを実行すると、以下のように画面上の2次元座標の位置として現在の状態が表示されます。
image

Processingでビジュアライズ 3 – タッチしていないときとの差分(距離)を表示

Touchéのジェスチャーは、じつはこのピークの際の登録したジェスチャーとの距離によって判別しています。では先程のプログラムを改良して、タッチしていないときの位置を記録しておき、現在の最大値と比較するようにしてみたいと思います。

このプログラムでは、まず画面をクリックするとその時点での最大値を記録します。記録した最大値と現在の最大値との間に線を描き、さらにその距離を表示しています。

Processing側 – Processing_graph.pde

/*
 * Touche for Arduino
 * Vidualization Example 03
 *
 */

float recVoltageMax;
float recTimeMax;
float voltageMax; //電圧の最大値
float timeMax; //電圧が最大値だったときの時間
float yMax = 100; //グラフのY座標最大値
float yMin = 0; //グラフのY座標最小値
float graphMargin = 20; //グラフと画面の余白

void setup() {
  //画面サイズ
  size(800, 600); 
  noLoop();
  //ポートを設定
  PortSelected=5; 
  //シリアルポートを初期化
  SerialPortSetup();

  //記録した最大値の値を初期化
  recVoltageMax = recTimeMax = 0;
}

void draw() {
  background(63);

  //最大値を0に初期化
  voltageMax = timeMax = 0;

  if ( DataRecieved3 ) {
    //電圧の最大値と、そのときの時間を取得
    for (int i = 0; i < Voltage3.length; i++) {
      if (voltageMax < Voltage3[i]) {
        voltageMax = Voltage3[i];
        timeMax = Time3[i];
      }
    }
    //画面に描画するために、(x, y)座標の値を画面の大きさにあわせて変換
    float x = map(timeMax, 0, 159, graphMargin, width-graphMargin);
    float y = map(voltageMax, yMin, yMax, height-graphMargin, graphMargin); 
    float rx = map(recTimeMax, 0, 159, graphMargin, width-graphMargin);
    float ry = map(recVoltageMax, yMin, yMax, height-graphMargin, graphMargin);

    //枠線を描く
    noFill();
    stroke(127);
    rect(graphMargin, graphMargin, width - graphMargin * 2, height - graphMargin * 2);

    //現在の最大値の座標を交差する線で描く
    stroke(80);
    line(x, graphMargin, x, height-graphMargin);
    line(graphMargin, y, width-graphMargin, y);

    //現在のそれぞれの最大値を文字で表示
    fill(#3399ff);
    noStroke();
    text(timeMax, x+2, height-graphMargin-2);
    text(voltageMax, graphMargin, y-10);

    //現在の最大値と、記録した最大値の間に線を描く
    stroke(255);
    line(x, y, rx, ry);
    //記録しておいた最大値の場所に円を描く
    ellipse(rx, ry, 20, 20);
    //現在の最大値の場所に円を描く
    ellipse(x, y, 20, 20);

    //現在の最大値と記録した最大値との距離を算出してテキストで表示
    float dist = dist(x, y, rx, ry);
    text("dist = "+dist, rx + 12, ry);
  }
}

//マウスをクリック
void mouseReleased() {
  //現在の最大値を記録
  recVoltageMax = voltageMax;
  recTimeMax = timeMax;
}

void stop() {
  myPort.stop();
  super.stop();
}

image

Processingでビジュアライズ 4 – 円の色と大きさで、差分を表現

もう少し表示を工夫してみましょう。

マウスをクリックして登録した際の位置を、常に画面の中央になるように補正します。そして、現在のピークの位置と記録したピークの位置の差分を円の半径と透明度に、また、X座標(時間)の差分を角度に対応させて画面に描いてみます。

Processing側 – Processing_graph.pde

/*
 * Touche for Arduino
 * Vidualization Example 04
 *
 */

float recVoltageMax;
float recTimeMax;
float voltageMax; //電圧の最大値
float timeMax; //電圧が最大値だったときの時間
float yMax = 100; //グラフのY座標最大値
float yMin = 0; //グラフのY座標最小値
float graphMargin = 20; //グラフと画面の余白

void setup() {
  //画面サイズ
  size(800, 600); 
  noLoop();
  //ポートを設定
  PortSelected=5; 
  //シリアルポートを初期化
  SerialPortSetup();
}

void draw() {
  background(63);

  //最大値を0に初期化
  voltageMax = timeMax = 0;

  if ( DataRecieved3 ) {
    //電圧の最大値と、そのときの時間を取得
    for (int i = 0; i < Voltage3.length; i++) {
      if (voltageMax < Voltage3[i]) {
        voltageMax = Voltage3[i];
        timeMax = Time3[i];
      }
    }
    //画面に描画するために、(x, y)座標の値を画面の大きさにあわせて変換
    float x = map(timeMax, 0, 159, 0, width);
    float y = map(voltageMax, yMin, yMax, height, 0); 
    float rx = map(recTimeMax, 0, 159, 0, width);
    float ry = map(recVoltageMax, yMin, yMax, height, 0);
    float diffX = x - rx;
    float diffY = y - ry;
    float dist = dist(x, y, rx, ry);
    float rot = map(diffX, 0, width, 0, PI*2);
    float col = map(dist, 0, width, 0, 255);

    pushMatrix();
    translate(width/2, height/2);
    rotate(rot);

    //現在の最大値と、記録した最大値の距離で円を描く
    fill(255, 0, 0, col);
    stroke(127);
    ellipse(0, 0, dist, dist);

    //現在の最大値と、記録した最大値の間に線を描く
    stroke(255);
    line(0, 0, dist/2, 0);

    //記録しておいた最大値の場所に円を描く
    noStroke();
    fill(#3399ff);
    ellipse(0, 0, 10, 10);

    //現在の最大値の場所に円を描く
    ellipse(dist/2, 0, 10, 10);
    popMatrix();

    //現在の最大値と記録した最大値との距離を算出してテキストで表示
    fill(255);
    text("dist = "+dist, 20, 20);
  }
}

//マウスをクリック
void mouseReleased() {
  //現在の最大値を記録
  recVoltageMax = voltageMax;
  recTimeMax = timeMax;
}

void stop() {
  myPort.stop();
  super.stop();
}

image

登録したジェスチャーと現在との違いが、より直感的に把握できるようになりました。

Processingでビジュアライズ 4 – 3Dで表現

最後に少し派手なビジュアライズのサンプルを紹介します。

角度をすこしずつ変更しながら大量の立方体を描くことで、複雑な形態のアニメーションを生成することが可能です。この例ではその立方体の大きさをピークの差分に。また角度もそれぞれの座標の差分で変化させることで、ジェスチャーから複雑に変化する3Dの形態を生みだしています。

Processing側 – Processing_graph.pde

/*
 * Touche for Arduino
 * Vidualization Example 05
 *
 */

float recVoltageMax;
float recTimeMax;
float voltageMax; //電圧の最大値
float timeMax; //電圧が最大値だったときの時間
float yMax = 100; //グラフのY座標最大値
float yMin = 0; //グラフのY座標最小値
float graphMargin = 20; //グラフと画面の余白

float angle = 0;   
int NUM = 100;   
float offset = 360.0/float(NUM);          
color[] colors = new color[NUM];

void setup() {
  //画面サイズ
  size(800, 600, OPENGL); 
  noStroke();
  colorMode(HSB, 360, 100, 100, 100);
  frameRate(60);
  for (int i=0; i<NUM; i++) {
    colors[i] = color(offset*i, 70, 100, 31);
  }
  //noLoop();
  //ポートを設定
  PortSelected=5; 
  //シリアルポートを初期化
  SerialPortSetup();
}

void draw() {
  background(0);

  //最大値を0に初期化
  voltageMax = timeMax = 0;

  if ( DataRecieved3 ) {
    //電圧の最大値と、そのときの時間を取得
    for (int i = 0; i < Voltage3.length; i++) {
      if (voltageMax < Voltage3[i]) {
        voltageMax = Voltage3[i];
        timeMax = Time3[i];
      }
    }
    //画面に描画するために、(x, y)座標の値を画面の大きさにあわせて変換
    float x = map(timeMax, 0, 159, 0, width);
    float y = map(voltageMax, yMin, yMax, height, 0);
    //記録した座標を変換
    float rx = map(recTimeMax, 0, 159, 0, width);
    float ry = map(recVoltageMax, yMin, yMax, height, 0);
    //それぞれの差分
    float diffX = x - rx;
    float diffY = y - ry;
    //距離を算出
    float dist = dist(x, y, rx, ry);

    //回転する立方体を角度をずらしながら大量に描く
    //立方体の大きさを差分に対応させている
    ambientLight(0, 0, 50);
    directionalLight(0, 0, 100, -1, 0, 0);
    pushMatrix();
    translate(width/2, height/2, -20); 
    for (int i=0; i<NUM; i++) {
      pushMatrix();
      fill(colors[i]);
      rotateY(radians(angle / diffY * 3.0 + offset*i));
      rotateX(radians(angle / diffY * 2.0 + offset*i));
      rotateZ(radians(angle / 10.0 + offset*i));
      box(dist);
      popMatrix();
    }
    angle += 1.0;
    popMatrix();
    //現在の最大値と記録した最大値との距離を算出してテキストで表示
    fill(0, 0, 100);
    text("dist = "+dist, 20, 20);
  }
}

//マウスをクリック
void mouseReleased() {
  //現在の最大値を記録
  recVoltageMax = voltageMax;
  recTimeMax = timeMax;
}

void stop() {
  myPort.stop();
  super.stop();
}

image


Processing.jsによるデータの可視化と生成的表現

今回からは、最終プロジェクトの制作むけて、Processing.jsを用いた実践的な表現手法についてとりあげていきます。まず、今回はデータのビジュアライズ(可視化)とジェネラティブ(Generative = 生成的)な表現について考えていきます。


Tumblrを使う2 – テーマをカスタマイズする

今回も引き続きTumblrを使用したWebサイト制作について解説していきます。Tumblrでは「テーマ」を選択することで誰でも簡単にデザインを変更することが可能です。さらに多くのテーマは色やフォントなど様々なページ内のデザイン要素をカスタマイズしていく機能が提供されています。テーマをカスタマイズすることで自分の好みにあわせたページを作っていくことができるようになります。今回はこのテーマのカスタマイズ方法についてとりあげていきます。

スライド資料

授業のスライドは、下記から閲覧してください。


Touchéの仕組みを理解 – ProcessingとArduinoの連携

今回は、前回作成したTouchéセンサーの、主にソフトウェアの仕組みについて解析して理解していきます。Touchéでは、ProcessingとArduinoが相互に連携して機能しています。この連携にはシリアル通信という仕組みを仕様しています。シリアル通信をProcessing、Arduino間で行う方法について、様々な手法を学んでいきます。

サンプルプログラム

サンプルプログラムは下記のリンクからダウンロードしてください。


Github Pagesで作品を公開、Automatic Page Generatorでサイトを生成

今回も引き続きGitHub Pagesを使用して作品をWebで発表する方法について解説していきます。今回はいよいよ作成したProcessing.jsの作品をGitHub Pagesに掲載する方法について試していきます。まず始めに、Web書き出ししたページをそのまま載せる方法を試します。次に、GitHub Pagesで用意されていページの自動生成機能、Automatic Page Generatorを使用したページ生成に挑戦します。


TumblrでWebサイトを作成

演習の後半は、また新たな内容に入っていきます。今回から数回にわたって、Tumblr(タンブラー)というWebサービスを利用してWebサイトを作成してみます。Tumblrは、メディアミックスブログサービスで、ブログとミニブログ、そしてソーシャルブックマークを統合したマイクロブログサービスとなっています。今回は導入としてメンバー登録と基本的な使い方について解説していきます。

スライド

スライド資料は下記から閲覧してください。


Touchéセンサーを使う1 Touchéセンサーを作成する

今回からは、いよいよTouchéセンサーを自作してタッチ検出に挑戦します。まずは、Touchéのセンサーのハードウェアを制作します。センサーが完成したら、水を入れたコップ、金属製の製品、植物など様々な物体でタッチ検出できるか試してみましょう。後半は、ソフトウェアを解析して、表示や表現の方法を変更するにはどう改造していけば良いのか考えていきます。

リンク

TouchéのProcessingとArduinoのアプリケーションは下記からダウンロードします