【p5.js】勝手にアニメーションアルバムジャケット制作【Little Dark Age/MGMT】

今回はMGMTの4thアルバム「Little Dark Age」風のアニメーションアルバムジャケットを作りました。

題材とするアルバム

題材とするのはMGMTの「Little Dark Ages」です。アルバムジャケットは以下の図

MGMTの4thアルバム「Little Dark Age」のジャケット

MGMTはサイケポップという一風変わったジャンルの音楽ユニットで、サイケデリックで不思議なサウンドなのにポップで聞きやすく仕上げているセンス抜群の楽曲が多数あります!1stアルバムの「Oracular Spectacular」(”謎めいた特別ショー”の意味)ではグラミー賞を獲得しており、高い評価を受けています。

↓は収録曲の「When You Die」です。少しグロテスクなので苦手な方は注意。


↓は4thアルバム「Little Dark Age」に収録されていませんが、1stアルバムの「Oracular Spectacular」に収録されている一番有名な楽曲の「kids」


アルバムジャケットをp5.jsで再現

実行結果

以下が再現したアルバムジャケットです。ズルいですが、今回はアルバムジャケットの元ネタ画像を拝借し、それを加工することで再現しました。元ネタはジミ・テーバー氏の作品で、同人誌「Witness to the Bizarre」の表紙に使用されたもののようです。

こちらが再現したアルバムジャケット
こちらが元画像

プログラム

画像の読み込み

まずは画像をloadImage()で読み込み、その後loadPixels()でピクセルごとの色データを取得しています。

JavaScript
function preload() {
  // 画像の読み込み
  // 処理が重たいためピクセル数の少ない画像が良い
  img = loadImage('pic.png');
}


function setup() {
  // 画像に合わせてキャンバスサイズを変える
  c = createCanvas(img.width*4, img.height*4);
  frameRate(FRAMERATE);

  // 1ピクセルごとにカラーコードを取得
  for (let xGrid = 0; xGrid < img.width; xGrid++) {
    let xyPixelsSub=[];
    for (let yGrid = 0; yGrid < img.height; yGrid++) {
      // 
      img.loadPixels();
      
      // カラーコードを取得(RGB)
      let c = color(img.get(xGrid, yGrid));


      // グレイスケールに変換
      let greyscale = round(red(c) * 0.222 + green(c) * 0.707 + blue(c) * 0.071);
      xyPixelsSub[yGrid]=greyscale;
    }
    xyPixels[xGrid]=xyPixelsSub;

  }
}

loadImage()はsetup()の前にpreload()内で実行することが推奨されています。画像の読み込みには時間がかかるようで、プログラム実行前に確実に読み込みを完了するためです。

読み込んだ画像から、loadPixels()によりカラーコード(RGB(A))を1ピクセル毎に取得します。この時、ピクセル数が多いと処理に長い時間がかかるため、本プログラムを使用する場合はピクセル数の少ない画像をお勧めします。

取得したカラーコードをグレイスケールに変換し、1ピクセル毎のグレイスケール値を格納しています。この値を基にドットのサイズを調整しています。

ドットの描画

次に、ドットのサイズと位置を調整して描画します。元画像はピクセル数が小さいので、元画像1ピクセルに対応したxy座標の変化量を設定します。そして、先ほどのグレイスケールを基にドットのサイズを調整してellipse()でドットを描きます。

JavaScript
function setup() {
  // 元画像1ピクセルに対応したxy座標の変化量を設定
  xTile = (width-2*MARGIN) / img.width;
  yTile = (height-2*MARGIN) / img.height;
}

function draw(){
  background(246, 217, 81);
  
  for (let xGrid = 0; xGrid < img.width; xGrid++) {
    for (let yGrid = 0; yGrid < img.height; yGrid++) {
      // 1ピクセル毎の描画位置を決める
      let posX = MARGIN+xTile*xGrid;
      let posY = MARGIN+yTile*yGrid;

      // グレイスケールからドットのサイズを決める
      let sizeEllipse = map(xyPixels[xGrid][yGrid], 0, 255, 20, 0);

      noStroke();
      fill(0);
      ellipse(posX, posY, sizeEllipse*0.3, sizeEllipse*0.3);
    }

  }
}

以下がプログラムの全文です。

JavaScript
// フレームレート
const FRAMERATE = 30;
// 余白
const MARGIN = 10;

let img;
let xTile;
let yTile;
let xyPixels=[];
let cnt=0;

function preload() {
  // 画像の読み込み
  // 処理が重たいためピクセル数の少ない画像が良い
  img = loadImage('pic.png');
}

function setup() {
  // 画像に合わせてキャンバスサイズを変える
  c = createCanvas(img.width*4, img.height*4);
  frameRate(FRAMERATE);

  // 1ピクセルごとにカラーコードを取得
  for (let xGrid = 0; xGrid < img.width; xGrid++) {
    let xyPixelsSub=[];
    for (let yGrid = 0; yGrid < img.height; yGrid++) {
      // 各ピクセルの配列を取得
      img.loadPixels();

      // カラーコードを取得(RGB)
      let c = color(img.get(xGrid, yGrid));

      // グレイスケールに変換
      let greyscale = round(red(c) * 0.222 + green(c) * 0.707 + blue(c) * 0.071);
      xyPixelsSub[yGrid]=greyscale;
    }
    xyPixels[xGrid]=xyPixelsSub;
  }

  // 元画像1ピクセルに対応したxy座標の変化量を設定
  xTile = (width-2*MARGIN) / img.width;
  yTile = (height-2*MARGIN) / img.height;
}

function draw(){
  background(246, 217, 81);

  for (let xGrid = 0; xGrid < img.width; xGrid++) {
    for (let yGrid = 0; yGrid < img.height; yGrid++) {
      // 1ピクセル毎の描画位置を決める
      let posX = MARGIN+xTile*xGrid;
      let posY = MARGIN+yTile*yGrid;

      // グレイスケールからドットのサイズを決める
      let sizeEllipse = map(xyPixels[xGrid][yGrid], 0, 255, 20, 0);
      
      // ドットのサイズをノイズで時間変化させる
      let noiseEllipse = noise(0.02*(xGrid+cnt), 0.02*(yGrid+cnt));
      noiseEllipse = map(noiseEllipse, 0, 1, 0.1, 1.9);

      // ドットの位置をノイズで時間変化させる
      let noisePos = noise(100+0.02*(xGrid+cnt), 100+0.02*(yGrid+cnt))
      noisePos = map(noisePos, 0, 1, -10, 10);
      
      // ドットがランダムに現れるようにする
      let a = random(0, 99);
      if(a>=10){
        noStroke();
        fill(0);
        ellipse(noisePos+posX, noisePos+posY, noiseEllipse*sizeEllipse*0.3, noiseEllipse*sizeEllipse*0.3);
      }
    }
  }
  cnt++;
}

アルバムジャケットにアニメーションを付ける

実行結果

次にアルバムジャケットを動かしていきます。ホラーな感じにしてみました。マウス操作とキー入力に対応していますので動かしてみてください。

  • 「1」or「2」→ 色変更
  • マウス操作→目が動く
  • 左クリック→画像変更

プログラム

キー操作とマウス操作

p5.jsではキー操作やマウス操作を受け取ることが可能で、それに応じて変数の値を変えることができます。今回の例ではキーで「1」,「2」の入力およびマウスのクリックで変数の値が変わるようにしています。

JavaScript
let mode=1;
let flag=0;

function keyReleased() {
  if (key == '1') mode=1;
  if (key == '2') mode=2;
}

function mouseClicked() {
  if (flag===0) {
    mode=2;
    flag=1;
  } else {
    mode=1;
    flag=0;
  }
}

ベクトル

目の動きはベクトルを使って表現しました。指定ポイントとマウスのホバー位置からベクトルを作成し、そのあとlimit()を用いることで長さを8に制限し、指定ポイントを中心に円運動を行うようにしています。
こちらの公式リファレンスがわかりやすいのでご覧ください。

JavaScript
let vector=createVector(mouseX-posX, mouseY-posY);
vector.limit(8);

以下がプログラムの全文です。

JavaScript
// フレームレート
const FRAMERATE = 30;
// 余白
const MARGIN = 10;

let img;
let imgEye;
let xTile;
let yTile;
let xyPixels=[];
let xyColors=[];
let cnt=0;
let mode=1;
let flag=0;

function preload() {
  // 画像の読み込み
  // 処理が重たいためピクセル数の少ない画像が良い
  img = loadImage('pic.png');
  imgEye = loadImage('eye.png');
}

function setup() {
  // 画像に合わせてキャンバスサイズを変える
  c = createCanvas(img.width*4, img.height*4);
  frameRate(FRAMERATE);

  // 1ピクセルごとにカラーコードを取得
  for (let xGrid = 0; xGrid < img.width; xGrid++) {
    let xyPixelsSub=[];
    let xyColorsSub=[];
    for (let yGrid = 0; yGrid < img.height; yGrid++) {
      // 各ピクセルの配列を取得
      img.loadPixels();

      // カラーコードを取得(RGB)
      let c = color(img.get(xGrid, yGrid));
      xyColorsSub[yGrid]=c;

      // グレイスケールに変換
      let greyscale = round(red(c) * 0.222 + green(c) * 0.707 + blue(c) * 0.071);
      xyPixelsSub[yGrid]=greyscale;
    }
    xyPixels[xGrid]=xyPixelsSub;
    xyColors[xGrid]=xyColorsSub;
  }

  // 元画像1ピクセルに対応したxy座標の変化量を設定
  xTile = (width-2*MARGIN) / img.width;
  yTile = (height-2*MARGIN) / img.height;
  // createLoop({duration:10, gif:true, framesPerSecond:FRAMERATE/5})
}

function draw(){
  if(mode==1){
    background(246, 217, 81);
  }else if(mode==2){
    background(50);
  }

  for (let xGrid = 0; xGrid < img.width; xGrid++) {
    for (let yGrid = 0; yGrid < img.height; yGrid++) {
      // 1ピクセル毎の描画位置を決める
      let posX = MARGIN+xTile*xGrid;
      let posY = MARGIN+yTile*yGrid;

      // グレイスケールから円のサイズを決める
      let sizeEllipse = map(xyPixels[xGrid][yGrid], 0, 255, 20, 0);
      
      // ドットのサイズをノイズで時間変化させる
      let noiseEllipse = noise(0.02*(xGrid+cnt), 0.02*(yGrid+cnt));
      noiseEllipse = map(noiseEllipse, 0, 1, 0.1, 1.9);

      // ドットの位置をノイズで時間変化させる
      let noisePos = noise(100+0.02*(xGrid+cnt), 100+0.02*(yGrid+cnt))
      noisePos = map(noisePos, 0, 1, -10, 10);
      
      // ドットがランダムに現れるようにする
      let a = random(0, 99);
      if(a>=10){
        noStroke();
        
        if(mode==1){
          fill(0);
        }else if(mode==2){
          fill(xyColors[xGrid][yGrid]);
        }
        ellipse(noisePos+posX, noisePos+posY, noiseEllipse*sizeEllipse*0.3, noiseEllipse*sizeEllipse*0.3);
      }

      // 右目
      if(xGrid==63&&yGrid==89){
        let vector=createVector(mouseX-posX, mouseY-posY);
        vector.limit(8);
        
        let xPosEye = noisePos+posX+vector.x;
        let yPosEye = noisePos+posY+vector.y;
        ellipse(xPosEye, yPosEye,8,8);
        if(flag){
          image(imgEye, noisePos+posX-15, noisePos+posY-15);
        }
      }
      // 左目
      if(xGrid==82&&yGrid==89){
        let vector=createVector(mouseX-posX, mouseY-posY);
        vector.limit(8);

        let xPosEye = noisePos+posX+vector.x;
        let yPosEye = noisePos+posY+vector.y;
        ellipse(xPosEye, yPosEye,8,8);
        if(flag){
          image(imgEye, noisePos+posX-15, noisePos+posY-15);
        }
      }
    }
  }
  cnt++;
}

function keyReleased() {
  if (key == '1') mode=1;
  if (key == '2') mode=2;
}

function mouseClicked() {
  if (flag===0) {
    mode=2;
    flag=1;
  } else {
    mode=1;
    flag=0;
  }
}

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

CAPTCHA