3分で読めるクリエイティブコーディングの小ネタ紹介です。
今回のテーマは「オブジェクトの一部を重ねながら綺麗にドーナツ状に配置するには?」です。
要点は、センバク(@senbaku)さんのツイートで問題提起されていた、「単純に重ねていくと、最後のオブジェクトが最初のオブジェクトの上に重なってしまう。これを、重なりが循環するように見せるにはどうしたらよいか?」というものです。
考え中🤔 pic.twitter.com/mK5pUTUYNb
— センバク (@senbaku) October 25, 2025
その後、なんとセンバク(@senbaku)さんのお子さんが、この問題の解法を思いつかれたそうです。
なるほどなー。
— センバク (@senbaku) October 25, 2025
こども案、円周上に並べた円の最初と最後に、半円を追加することで重なりをいい感じに見せる、というものだった。簡単でスマートやん...。#p5js pic.twitter.com/nWNpUWU12i
最初と最後を半円にするというシンプルで美しい解法です。このアイデアは素晴らしいですね!
このアイデアをヒントに、 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
);
}
コードにはちょっと"力技"感がありますね。描画結果はこうなります。
【発想の転換】そもそも「重ねる」のをやめる
そもそもの問題は「最後のオブジェクトが最初のオブジェクトの上に重なってしまう」、つまり「重なる」ことが問題なのだから、オブジェクトを重ねずに済むものにすればよいのでは?
具体的には、下図を一つのオブジェクトと考えたらどうでしょうか?
これを繋げていけば、オブジェクトを重ねずに済みます。
/**
* クリコー 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つの方法は、オブジェクトが真円なら良いけれど、もっと複雑な形だとそのままでは実現が難しいと思います。
例えばこのようなオブジェクトでも対応できるような、もっと汎用的な方法はないでしょうか?
オブジェクトを小さな createGraphics() 上に描画すれば、それを image() で並べることもできますし、上下半分に切って使うこともできるので、「重なりのまずい所だけ描き直す(案1)」の汎用版になりそうです。
createGraphics() 上に描画するものはどんなに複雑なものでも対応可能です。
他にも、 createGraphics() 上に描画した後に、オブジェクトの下側を半透明から透明にグラデーションをかけて、それを並べるという方法も考えられます。
重なって下になる部分を半透明で「うっすら」と描くわけです。
これを 100回繰り返すと、このとおり。
この方法だとループだけで完結するし、 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();
}
}
}
まとめ
ここまで、オブジェクトの一部を重ねながら綺麗にドーナツ状に配置する方法をいくつか考えてみました。それぞれの方法のメリット・デメリットをまとめてみましょう。
| 方法 | メリット | デメリット | 向いているケース |
|---|---|---|---|
| 重なりのまずい所だけ描き直す | 実装がシンプル | 複雑な描画への対応が困難 | 円や単純な図形で、とにかく早く結果を出したい場合に有効 |
| そもそも「重ねる」のをやめる | 重なり順の問題が原理的に発生しないため、エレガント | 実装が難しい 複雑な描画への対応が困難 | 実装は大変だが、このテクニック自体が他の問題解決のヒントになる可能性がある |
| 描画に工夫をして「うっすら」重ねる | 汎用性が高く、複雑な描画へも対応できる | 処理が重くなる可能性 | 表現にこだわりたいクリエイティブ・コーディングでは、最も実用性の高い選択肢 |
クリエイティブコーディングのいろんな工程の中でも、「どうやったらできるかな?」と考える時間には格別な楽しさがあります。
今回の問題にはきっと他にもユニークな解法があることでしょう。ぜひ、ご自身のコードで「どうやったらできるかな?」というクリエイティブ・コーディングの醍醐味を味わってみてください。















