回転運動の再帰で作ったデジタルアートの制作過程

2021年6月13日日曜日

Processing 作例 静止画 中級者向け

t f B! P L

👉 Read this article in English.

中央に面白い紋様、その周りを細かい円と四角形が埋める。

概略

Processing を使ったクリエイティブ・コーディングによるデジタルアートです。

再帰による回転運動の計算により中央の紋様を生成、その周辺を埋める円と四角形はサークル・パッキングの手法を用いて描いています。

どのように作っていったか?

最初のアイディアはひさだん(@hisadan)さんのツイートでした。

再帰関数により円の入れ子を実現しています。

これがとても面白く、いろいろと描画を変えて遊んでいました。
入れ子の各円の中心の軌道上に点をプロットしたもの。


同様に円を描画した静止画。

軌道上に多数の円を描画したもの

軌道を曲線で描いたもの。

軌道を曲線で描いたもの

遊んでいるうちに、再帰関数の計算式を変えると面白い形が描画されることに気が付きました。

再帰関数の計算式を変えて描いた面白い形

この面白い形を活かすことにし、このままではちょっと寂しいので、周辺や隙間をサークル・パッキングの手法で埋めることにしました。

面白い形と、その周辺をサークル・パッキングの手法で埋めた画像

サークル・パッキングでは新しく円を描く際に、既に円が存在する場所を避けるようにロジックが組まれています。そこで、面白い形を描画するための頂点を避けるべき場所としてあらかじめ設定することで、周辺と隙間だけをサークル・パッキングで埋められるようにしました。

面白い形を描画するための頂点を円で描画

配色はいろいろ迷った挙げ句、いまでもこれでいいのかよくわからないままです。

配色を試している途中

配色を試している途中

"Processing" のサンプルコード

GPL で公開します。GPL の条項に基づいて、どうぞご自由にお使いください。このコードを利用して何か作品を作ってもらえるととても嬉しいです。

本コードはスクリーン上には何も描画しません。描画の結果はファイルとして保存されます。


/**
 * Under the Waterfall.
 * draw shapes with the calculation of rotary movement recursively.
 *
 * @author @deconbatch
 * @version 0.1
 * @license GPL Version 3 http://www.gnu.org/licenses/
 * Processing 3.5.3
 * 2021.06.11
 */

void setup() {
  size(980, 980);
  colorMode(HSB, 360.0, 100.0, 100.0, 100.0);
  rectMode(CENTER);
  smooth();
  noLoop();
}

void draw(){

  int   ptnMax  = 3;
  float baseHue = random(360.0);

  for (int ptnCnt = 0; ptnCnt < ptnMax; ptnCnt++) {

    baseHue += 90.0;

    // background image
    noiseSeed(floor(baseHue + ptnCnt));
    image(noiseField(baseHue), 0.0, 0.0);

    // foreground image
    ArrayList<ArrayList<PVector>> shapes = getShapes();
    PGraphics fg = createGraphics(width, height);
    fg.beginDraw();
    fg.colorMode(HSB, 360.0, 100.0, 100.0, 100.0);
    fg.rectMode(CENTER);
    fg.translate(width * 0.5, height * 0.5);
    fg.background(0.0, 0.0, 60.0, 100.0);
    scabrous(fg, baseHue);
    lattice(fg, baseHue);
    drawShape(fg, baseHue, shapes);
    drawBubbles(fg, baseHue, shapes);
    fg.endDraw();
    image(fg, 0.0, 0.0);

    // fancy casing
    casing();

    saveFrame("frames/" + String.format("%04d", ptnCnt + 1) + ".png");
  }
  exit();
}

/**
 * noiseField : draw noise field
 */
PGraphics noiseField(float _hue) {

  int   cellSize = 3;
  float noiseDiv = 0.01;
  
  PGraphics p = createGraphics(width, height);
  p.beginDraw();
  p.colorMode(HSB, 360.0, 100.0, 100.0, 100.0);
  p.noStroke();
  for (int bx = 0; bx < width; bx += cellSize) {
    float nx = bx * noiseDiv;
    for (int by = 0; by < height; by += cellSize) {
      float ny = by * noiseDiv;
      float nHue = noise(nx, ny, noise(10.0, nx, ny) * 5.0);
      float nSat = noise(nx, ny, noise(20.0, nx, ny) * 8.0);
      float nBri = noise(nx, ny, noise(30.0, nx, ny) * 10.0);
      p.fill(
             (_hue + nHue * 120.0) % 360.0,
             30.0 + nSat * 60.0,
             30.0 + nBri * 60.0,
             100.0
             );
      p.rect(bx, by, cellSize, cellSize);
    }
  }
  p.endDraw();

  return p;
}

/**
 * getShapes : get shapes locations
 */
ArrayList<ArrayList<PVector>> getShapes() {
  int   rotaryMax   = 72;
  int   cycleMax    = floor(random(1.0, 5.0));
  float cycleRadius = min(width, height) * random(4.0);
  float phase       = floor(random(8.0)) * 0.5 * HALF_PI;

  ArrayList<ArrayList<PVector>> shapes = new ArrayList<ArrayList<PVector>>();
  for (int rotaryCnt = 0; rotaryCnt < rotaryMax; rotaryCnt++) {
    float rotation = map(rotaryCnt, 0, rotaryMax, 0.0, TWO_PI);
    ArrayList<PVector> p = new ArrayList<PVector>();
    shapes.add(calcRotary(
                          0,
                          p,
                          cycleRadius * cos(rotation * cycleMax),
                          cycleRadius * sin(rotation * cycleMax),
                          width,
                          rotation + phase
                          ));
  }

  // size adjust
  float minX = width * 10.0;
  float minY = height * 10.0;
  float maxX = -minX;
  float maxY = -minY;
  for (ArrayList<PVector> shape : shapes) {
    for (PVector v : shape) {
      minX = min(minX, v.x);
      minY = min(minY, v.y);
      maxX = max(maxX, v.x);
      maxY = max(maxY, v.y);
    }
  }
  float adjust = 0.7 * min(width / (maxX - minX), height / (maxY - minY));
  for (ArrayList<PVector> shape : shapes) {
    for (PVector v : shape) {
      v.mult(adjust);
    }
  }
    
  return shapes;
}

/**
 * calcRotary : calculate rotary movement recursively
 */
ArrayList<PVector> calcRotary(int _cnt, ArrayList<PVector> _p, float x, float y, float d, float r){

  if (_cnt > 10) {
    return _p;
  }

  float nx = d * cos(r) + x;
  float ny = d * sin(r) + y;
  _p.add(new PVector(nx, ny));
  _p = calcRotary(_cnt + 1, _p, nx, ny, d * 0.75, -r * 5.0);

  return _p;
}

/**
 * scabrous : draw scabrous surface
 */
void scabrous(PGraphics _p, float _hue) {
  for (int x = 0; x < width * 0.5; x += 3) {
    for (int y = 0; y < height * 0.5; y += 3) {
	
      float pSiz = random(0.5, 1.0);
      float pDiv = random(-2.0, 2.0);
      float pSat = 0.0;
      if ((x + y) % 3 == 0) {
        pSat = 80.0;
      }

      _p.strokeWeight(pSiz);
      _p.stroke((_hue + 240.0) % 360.0, pSat, 30.0, 100.0);
      _p.point(x + pDiv, y + pDiv);
      _p.point(-x + pDiv, y + pDiv);
      _p.point(x + pDiv, -y + pDiv);
      _p.point(-x + pDiv, -y + pDiv);
    }
  }
}

/**
 * lattice : draw lattice
 */
void lattice(PGraphics _p, float _hue) {
  for (int x = -width; x < width * 0.5; x += 50) {
    for (int y = -height; y < height * 0.5; y += 80) {
      _p.strokeWeight(random(10.0));
      _p.stroke((_hue + 210.0) % 360.0, random(5.0), 60.0, 100.0);
      _p.line(x - 50.0, y, x + 50.0, y);
      _p.line(x, y - 80.0, x, y + 80.0);
    }
  }
}

/**
 * drawShape : draw foreground shape
 */
void drawShape(PGraphics _p, float _hue, ArrayList<ArrayList<PVector>> _shapes) {

  _p.noFill();
  for (ArrayList<PVector> shape : _shapes) {

    // outer rim
    _p.blendMode(BLEND);
    _p.stroke(0.0, 0.0, 0.0, 100.0);
    _p.beginShape();
    for (PVector v : shape) {
      float d = dist(0.0, 0.0, v.x, v.y);
      _p.strokeWeight(3.0 + d * 40.0 / width);
      _p.curveVertex(v.x, v.y);
    }
    _p.endShape();

    _p.blendMode(BLEND);
    _p.stroke((_hue + 120.0) % 360.0, 20.0, 30.0, 100.0);
    _p.beginShape();
    for (PVector v : shape) {
      float d = dist(0.0, 0.0, v.x, v.y);
      _p.strokeWeight(1.0 + d * 40.0 / width);
      _p.curveVertex(v.x, v.y);
    }
    _p.endShape();

    // inner rim
    _p.blendMode(BLEND);
    _p.beginShape();
    for (PVector v : shape) {
      float d = dist(0.0, 0.0, v.x, v.y);
      if (d > width * 0.2) {
        _p.stroke(0.0, 0.0, 0.0, 100.0);
        _p.strokeWeight(2.0 + d * 10.0 / width);
      } else {
        _p.noStroke();
      }
      _p.curveVertex(v.x, v.y);
    }
    _p.endShape();

    // center hole to see the background
    _p.blendMode(REPLACE);
    _p.stroke(0.0, 0.0, 0.0, 0.0);
    _p.beginShape();
    for (PVector v : shape) {
      float d = dist(0.0, 0.0, v.x, v.y);
      _p.strokeWeight(1.0 + d * 10.0 / width);
      _p.curveVertex(v.x, v.y);
    }
    _p.endShape();
  }
}

/**
 * drawBubbles : draw circle packing around the shape
 */
void drawBubbles(PGraphics _p, float _hue, ArrayList<ArrayList<PVector>> _shapes) {

  // circle packing around the shape
  ArrayList<Circle> avoids = new ArrayList<Circle>();
  for (ArrayList<PVector> points : _shapes) {
    for (PVector v : points) {
      float d = dist(0.0, 0.0, v.x, v.y);
      avoids.add(new Circle(v.x, v.y, 20.0 + 0.2 * d));
    }
  }
  ArrayList<Circle> packed = circlePacking(avoids, 5.0);

  // circle packing background
  _p.blendMode(BLEND);
  _p.noStroke();
  for (Circle c : packed) {
    float dRatio = dist(0.0, 0.0, c.x, c.y) / max(width, height);
    float eR = c.r * (60.0 / (10.0 + c.r)) * (1.0 + dRatio);
    float eBri = 20.0 + 80.0 * constrain(dRatio, 0.3, 1.0);
    _p.fill((_hue + 150.0) % 360.0, 20.0, eBri, 100.0);
    _p.ellipse(c.x, c.y, eR, eR);
  }

  // draw packed circles hole
  _p.blendMode(REPLACE);
  _p.stroke(0.0, 0.0, 0.0, 100.0);
  for (Circle c : packed) {
    _p.fill(0.0, 0.0, 80.0, random(30.0, 100.0));
    _p.strokeWeight(c.r * 0.1);
    if (random(1.0) < 0.3) {
      _p.rect(c.x, c.y, c.r, c.r);
    } else {
      _p.ellipse(c.x, c.y, c.r, c.r);
    }
  }
    
}

/**
 * casing : draw fancy casing
 */
private void casing() {
  pushMatrix();
  translate(width * 0.5, height * 0.5);
  fill(0.0, 0.0, 0.0, 0.0);
  strokeWeight(44.0);
  stroke(0.0, 0.0, 0.0, 100.0);
  rect(0.0, 0.0, width, height);
  strokeWeight(40.0);
  stroke(0.0, 0.0, 100.0, 100.0);
  rect(0.0, 0.0, width, height);
  popMatrix();
}

/**
 * circlePacking : bloom circles with the Circle Packing method.
 * @param _av    : circles to avoid.
 * @param _gap   : gap between circles.
 */
private ArrayList<Circle> circlePacking(ArrayList<Circle> _av, float _gap) {

  int   tryMax  = 100;  // a trying count to add and grow circles.
  float growMax = 30.0;
  ArrayList<Circle> circles = new ArrayList<Circle>();

  for (int tryCnt = 0; tryCnt < tryMax; tryCnt++) {

    // add new circles on the grid
    for (int i = 0; i < 1000; i++) {
      float addX = floor(random(-50.0, 50.0)) * 0.01 * width;
      float addY = floor(random(-50.0, 50.0)) * 0.01 * height;
      boolean inner = false;
      for (Circle c : circles) {
        if (dist(addX, addY, c.x, c.y) < c.r + _gap) {
          inner = true;
          break;
        }
      }
      for (Circle c : _av) {
        if (dist(addX, addY, c.x, c.y) < c.r + _gap * 2.0) {
          inner = true;
          break;
        }
      }
      if (!inner) {
        circles.add(new Circle(addX, addY, 0.0));
      }
    }
  
    // grow circles
    for (Circle cThis : circles) {
      if (cThis.r < growMax) {
        int collision = 0;
        for (Circle cThat : circles) {
          if (cThis != cThat) {
            if (dist(cThis.x, cThis.y, cThat.x, cThat.y) < (cThis.r + cThat.r) * 0.5 + _gap) {
              collision++;
            }
          }
        }
        for (Circle cAvoid : _av) {
          if (dist(cThis.x, cThis.y, cAvoid.x, cAvoid.y) < (cThis.r + cAvoid.r) * 0.5 + _gap) {
            collision++;
          }
        }
        if (collision == 0) {
          cThis.grow();
        }
      }
    }

  }
  return circles;
}

/**
 * Circle : draw and hold location, size and color.
 */
public class Circle {

  private float x, y;     // coordinate
  private float r;        // radius

  Circle(float _x, float _y, float _r) {
    x = _x;
    y = _y;
    r = _r;
  }

  public void grow() {
    r++;
  }
}


/*
Copyright (C) 2021- deconbatch

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>
*/


おまけの再帰関数による円の入れ子のアニメーション


元のアイディアとなった再帰関数による円の入れ子のアニメーションのコードです。
p5.js のコードです。


// ref. https://twitter.com/hisadan/status/1398307061000146948

const w = 600;
const h = 600;
const frmRate = 24;
let r;

function setup() {
	createCanvas(w, h);
	frameRate(frmRate);
	r = 0;
}

function draw() {
	background(0);
	fill(255, 100);
	a(w * 0.5, h * 0.5, min(w, h) * 0.25, r);
	r += PI / (frmRate * 6);
}

function a(x, y, d, r) {
	if (d > 9) {
		push();
		translate(x, y);
		let s = d * sin(r);
		let c = d * cos(r);
		circle(0, 0, d * 4);
		a(s, c, d / 2, r * 2);
		a(-s, -c, d / 2, -r * 2);
		pop();
	}
}


ついでに、円の中心部分だけを点で描画するとこんな感じに。



QooQ