【3分で読めるクリエイティブ・コーディング】:ドーナツ状配置の「つなぎ目」問題を解決する方法

2025年10月30日木曜日

3分クッキング p5.js 作例

t f B! P L
タイトル画像

3分で読めるクリエイティブコーディングの小ネタ紹介です。
今回のテーマは「オブジェクトの一部を重ねながら綺麗にドーナツ状に配置するには?」です。

要点は、センバク(@senbaku)さんのツイートで問題提起されていた、「単純に重ねていくと、最後のオブジェクトが最初のオブジェクトの上に重なってしまう。これを、重なりが循環するように見せるにはどうしたらよいか?」というものです。


その後、なんとセンバク(@senbaku)さんのお子さんが、この問題の解法を思いつかれたそうです。

最初と最後を半円にするというシンプルで美しい解法です。このアイデアは素晴らしいですね!

このアイデアをヒントに、 p5.js で具体的な実装方法を考えてみます。

【力技?】重なりのまずい所だけ描き直す

今までいろんな事を誤魔化しながら生きてきた、ズルい大人である私が思い付くアイデアは、「まずい部分だけを取り繕う」というものです。

具体的には、最後のオブジェクトが最初のオブジェクトの上に重なってしまった部分だけを描画し直します。



/** 
 * クリコー 3分クッキング: 重なりを循環させる。
 * オブジェクトの一部を重ねながら綺麗にドーナツ状に配置する方法
 * 
 * p5.js サンプルコード: まずい部分を重ねなおす
 * 
 * @author @deconbatch
 * @version 0.1
 * @license CC0 1.0 https://creativecommons.org/publicdomain/zero/1.0/deed.ja
 * p5.js 1.11.11
 * created 2025.10.28
 */

function setup() {

  const CANVAS_SIZE      = 520;
  const DONUT_SIZE_RATIO = 0.3;
  const OBJ_NUM          = 8;
  const OBJ_SIZE         = DONUT_SIZE_RATIO * CANVAS_SIZE;

  createCanvas(CANVAS_SIZE, CANVAS_SIZE);
  translate(width * 0.5, height * 0.5);
  background("SteelBlue");
  fill("LightSteelBlue");
  stroke("Snow");
  strokeWeight(10);

  for (let t = 0; t < TWO_PI; t += TWO_PI / OBJ_NUM) {
    circle(
           OBJ_SIZE * cos(t),
           OBJ_SIZE * sin(t),
           OBJ_SIZE
    );
  }
  arc(
      OBJ_SIZE * cos(0),
      OBJ_SIZE * sin(0),
      OBJ_SIZE,
      OBJ_SIZE,
      PI,
      TWO_PI,
      OPEN
  );

}
 

コードにはちょっと"力技"感がありますね。描画結果はこうなります。

重なりが解決したドーナツ

【発想の転換】そもそも「重ねる」のをやめる

そもそもの問題は「最後のオブジェクトが最初のオブジェクトの上に重なってしまう」、つまり「重なる」ことが問題なのだから、オブジェクトを重ねずに済むものにすればよいのでは?

具体的には、下図を一つのオブジェクトと考えたらどうでしょうか?

半円が2個重なった図

これを繋げていけば、オブジェクトを重ねずに済みます。

重なりが解決したドーナツ

/** 
 * クリコー 3分クッキング: 重なりを循環させる。
 * オブジェクトの一部を重ねながら綺麗にドーナツ状に配置する方法
 * 
 * p5.js サンプルコード: 重ならないよう並べれば?
 * 
 * @author @deconbatch
 * @version 0.1
 * @license CC0 1.0 https://creativecommons.org/publicdomain/zero/1.0/deed.ja
 * p5.js 1.11.11
 * created 2025.10.28
 */

function setup() {

  const CANVAS_SIZE      = 520;
  const DONUT_SIZE_RATIO = 0.3;
  const OBJ_NUM          = 8;
  const OBJ_SIZE         = DONUT_SIZE_RATIO * CANVAS_SIZE;
  const OBJ_THETA_STEP   = TWO_PI / OBJ_NUM;

  createCanvas(CANVAS_SIZE, CANVAS_SIZE);
  translate(width * 0.5, height * 0.5);
  background("SteelBlue");
  fill("LightSteelBlue");
  stroke("Snow");
  strokeWeight(10);

  for (let t = 0; t < TWO_PI; t += OBJ_THETA_STEP) {
    push();
    translate(OBJ_SIZE * cos(t), OBJ_SIZE * sin(t));
    rotate(t);
    arc(
        0,
        0,
        OBJ_SIZE,
        OBJ_SIZE,
        0,
        PI,
        OPEN
    );
    arc(
        OBJ_SIZE * (cos(OBJ_THETA_STEP) - 1),
        OBJ_SIZE * sin(OBJ_THETA_STEP),
        OBJ_SIZE,
        OBJ_SIZE,
        PI + OBJ_THETA_STEP,
        TWO_PI + OBJ_THETA_STEP,
        OPEN
    );
    pop();
  }

}
 

ループの後の arc() が不要になり、ループだけで完結するのでコード的にエレガントな気もしますが、 arc() の位置計算の式が複雑で、汎用性や可読性の面でエレガントと言えるのかどうか微妙ですね。

【より汎用的に】描画に工夫をして「うっすら」重ねる

前述の 2つの方法は、オブジェクトが真円なら良いけれど、もっと複雑な形だとそのままでは実現が難しいと思います。

例えばこのようなオブジェクトでも対応できるような、もっと汎用的な方法はないでしょうか?

楕円を3個重ねて原子模型的なオブジェクト
2色で有機的な円をかたどったオブジェクト

オブジェクトを小さな createGraphics() 上に描画すれば、それを image() で並べることもできますし、上下半分に切って使うこともできるので、「重なりのまずい所だけ描き直す(案1)」の汎用版になりそうです。

重なりが解決したドーナツ

createGraphics() 上に描画するものはどんなに複雑なものでも対応可能です。

他にも、 createGraphics() 上に描画した後に、オブジェクトの下側を半透明から透明にグラデーションをかけて、それを並べるという方法も考えられます。
重なって下になる部分を半透明で「うっすら」と描くわけです。

半透明画像でドーナツ

これを 100回繰り返すと、このとおり。

重なりが解決したドーナツ

この方法だとループだけで完結するし、 BLUR をかけたりなどの様々な装飾の工夫も楽しめます。

BLURで装飾を施したドーナツ

/** 
 * クリコー 3分クッキング: 重なりを循環させる。
 * オブジェクトの一部を重ねながら綺麗にドーナツ状に配置する方法
 * 
 * p5.js サンプルコード: 『重ねちゃってもいいさ』と考えるんだ
 * 
 * @author @deconbatch
 * @version 0.1
 * @license CC0 1.0 https://creativecommons.org/publicdomain/zero/1.0/deed.ja
 * p5.js 1.11.11
 * created 2025.10.28
 */

function setup() {
  
  const CANVAS_SIZE      = 520;
  const DONUT_SIZE_RATIO = 0.3;
  const OBJ_NUM          = 8;
  const OBJ_SIZE         = DONUT_SIZE_RATIO * CANVAS_SIZE;
  const OBJ_THETA_STEP   = TWO_PI / OBJ_NUM;

  createCanvas(CANVAS_SIZE, CANVAS_SIZE);
  imageMode(CENTER);

  // オブジェクトの原画(ランダムに歪んだ四辺形)
  const obj = createGraphics(OBJ_SIZE, OBJ_SIZE);
  obj.background(0, 0);
  obj.fill("LightSteelBlue")
  obj.stroke("Snow");
  obj.strokeWeight(5);
  obj.push();
  obj.translate(OBJ_SIZE * 0.5, OBJ_SIZE * 0.5);
  for (let i = 0.8; i > 0; i -= 0.2) {
    let w = OBJ_SIZE * i;
    obj.beginShape();
    obj.vertex(random(-0.5, -0.4) * w, random(-0.5, -0.4) * w);
    obj.vertex(random(0.4, 0.5) * w, random(-0.5, -0.4) * w);
    obj.vertex(random(0.4, 0.5) * w, random(0.4, 0.5) * w);
    obj.vertex(random(-0.5, -0.4) * w, random(0.4, 0.5) * w);
    obj.endShape(CLOSE);
  }
  obj.pop();

  // オブジェクト原画を半透明化したもの
  const img = createGraphics(OBJ_SIZE, OBJ_SIZE);
  img.image(obj, 0, 0);
  img.strokeWeight(2);
  img.blendMode(REMOVE);
  for (let i = 0; i < OBJ_SIZE; i++) {
    img.stroke(0, map(i, 0, OBJ_SIZE, 0, 256));
    img.line(0, i, OBJ_SIZE, i);
  }

  // オブジェクト原画をぼかす
  obj.filter(BLUR, 10);
  
  translate(width * 0.5, height * 0.5);
  background("SteelBlue");

  // オブジェクト原画を並べる
  for (let t = 0; t < TWO_PI; t += OBJ_THETA_STEP) {
    push();
    translate(OBJ_SIZE * cos(t), OBJ_SIZE * sin(t));
    rotate(t);
    image(obj, 0, 0);
    pop();
  }

  // 半透明化オブジェクトを重ね塗り
  for (let i = 0; i < 20; i++) {
    for (let t = 0; t < TWO_PI; t += OBJ_THETA_STEP) {
      push();
      translate(OBJ_SIZE * cos(t), OBJ_SIZE * sin(t));
      rotate(t);
      image(img, 0, 0);
      pop();
    }
  }

}
 

まとめ

ここまで、オブジェクトの一部を重ねながら綺麗にドーナツ状に配置する方法をいくつか考えてみました。それぞれの方法のメリット・デメリットをまとめてみましょう。

方法メリットデメリット向いているケース
重なりのまずい所だけ描き直す実装がシンプル複雑な描画への対応が困難円や単純な図形で、とにかく早く結果を出したい場合に有効
そもそも「重ねる」のをやめる重なり順の問題が原理的に発生しないため、エレガント実装が難しい
複雑な描画への対応が困難
実装は大変だが、このテクニック自体が他の問題解決のヒントになる可能性がある
描画に工夫をして「うっすら」重ねる汎用性が高く、複雑な描画へも対応できる処理が重くなる可能性表現にこだわりたいクリエイティブ・コーディングでは、最も実用性の高い選択肢

クリエイティブコーディングのいろんな工程の中でも、「どうやったらできるかな?」と考える時間には格別な楽しさがあります。

今回の問題にはきっと他にもユニークな解法があることでしょう。ぜひ、ご自身のコードで「どうやったらできるかな?」というクリエイティブ・コーディングの醍醐味を味わってみてください。

QooQ