NFT アートの「Generativemasks」は、実行のたびに異なる色と模様が生成される、面白いタイプの NFT アートです。
👉 Read this article in English.
コードはオープンソースで、クリエイティブ・コモンズ BY-NC-SA 3.0 のライセンスに従うことで、自由に派生物を作ることが出来ます。
この派生物を作るのが面白そうだったので、私もやってみました。
Generativemasks abstraction. 🕵️♀️#p5js #creativecoding #Generativemasks pic.twitter.com/TdWFGllcAm
— deconbatch (@deconbatch) October 10, 2021
すると、オリジナルのコードをそのままで使うと、いろいろと制約が多いことに気づきました。
- キャンバスサイズを変えるとマスクの形状も変わる
- randomSeed() の影響がコード全体に及ぶ
- など
そこで、その制約を取り払うため、独立した JavaScript のモジュールを作成しました。サイズやカラーパレットを指定してやると、マスクを生成して p5.Graphics の形で返してくれるというものです。
これを使えばマスクの生成が簡単になり、生成したマスクをどう活用するかに神経を集中することができます。つまり、派生物の作成が超絶楽チンになるというわけです。
この JavaScript のモジュールを作例とともに紹介します。ぜひ、これを使って Generativemasks の派生物を作ってみてください。きっと楽しいですよ。
何を作ったか?
下記のように、Generativemasks のマスク生成の自由度を高めることができるコードです。
- サイズ可変
- カラーパレット指定可能(coolers URL 互換)
- 背景有無指定可能
- シャドー有無指定可能
- テクスチャ有無指定可能
名前は Garg (Generates A Resembling Generativemasks)。「ギャーグ」とでも呼んでください。
Garg では Generativemasks とほぼ同じ形、模様のマスクを生成できますが、全く同じではありません。マスク生成のコードにも手を入れており、このモジュールが生成する画像はオリジナルの Generativemasks のそれと正確に同じにはなりません。
『オリジナルが欲しければ、オリジナルを使えばいいじゃない』という考え(開き直り)です。
実装例
サイズやカラーパレット、背景の有無を指定して自分好みの Generativemasks を生成する、Garg の機能をフルに使った Web アプリケーション「Generativemasks to Your Taste」を作りました。
「Generativemasks to Your Taste」 OpenProcessing 上で動かせます。
下記設定で生成すると、オリジナルの Generativemasks とほぼ同様のものを生成できます。
- サイズ = 1600
- カラーパレット指定なし
- 背景あり
- シャドーあり
- テクスチャあり
その他の実装例
SYM さんの p5.pattern とのコラボ
SYM(@hyappy717) さん作のパターン生成ライブラリ p5.pattern を用いて Generativemasks の模様を描く Web アプリケーションです。Garg をいじって GargPattern というものを作り、それを使って作りました。
「Generativemasks meets p5.pattern」 OpenProcessing 上で動かせます。
実はこれをやりたくて Garg を作りました
マスクを高度に抽象化すると銘打って、"クリエイティブ・コーディングの定番「ノード・ガーデン」:応用編"という記事で書いた同じ色のノードを線で結ぶノード・ガーデンを Generativemasks に適用したものです。
「Generativemasks Abstractor」 OpenProcessing 上で動かせます。
低レベルの抽象化だとこういう絵になります。
'Garg' の使い方
シャドーあり、テクスチャなし、背景なしで、ID=37 のマスクを 480x480 サイズでキャンバスに描画するコード例です。
const gg = new Garg(true, false, false);
const mask = gg.createMask(37, 480);
image(mask, 0, 0);
mask.remove();
コンストラクタ
new Garg(s, t, b)
パラメータ
s boolean : true シャドーあり
t boolean : true テクスチャあり
b boolean : true 背景あり、 false 背景は透明
カラーパレットのチェック
メソッド
chkRgbStrings(s)
パラメータ
s String : カラーパレット
説明
カラーパレットの形式をチェックし、OK なら true、NG なら false を返します。
正しい形式であるには、RGB で RRGGBB-RRGGBB-RRGGBB の形、少なくとも 3色以上あることが必要です。隠し機能(隠してないけど)として、coolers の URL 形式そのままで使えます。
例:https://coolors.co/000000-14213d-fca311-e5e5e5-ffffff
マスクの生成
メソッド
createMask(i, s)
パラメータ
i Number : 生成する Generativemasks のマスク ID。オリジナルは 0 から 9999 ですが、Garg では 10000 以上も受け付けます。
s Number : 生成する画像のサイズ。必ず正方形を返すので、 s x s ピクセルの画像となります。
説明
マスクを生成し、p5.Graphics の形式で返します。返された p5.Graphics は要らなくなった時点で remove() してください。
指定したカラーパレットの適用
メソッド
setPalette(p)
パラメータ
p String : カラーパレット
説明
与えられたカラーパレットを適用します。このカラーパレットでマスクを生成するには、createMask() の前にこのメソッドを実行することが必要です。
カラーパレットの形式については chkRgbStrings() の説明を参照ください。
ランダムなカラーパレットの適用
メソッド
setRandomPalette()
パラメータ
なし
説明
Garg に内蔵されたカラーパレットからランダムに選択したパレットを適用します。
Garg のコード
最新の version 1.0a がこちら。長いので隠しています。
/*
* Garg : Generates A Resembling Generativemasks.
*
* Version : 0.1a
* Auther : deconbatch (https://www.deconbatch.com/)
* Revise : tetunori
* License : Creative Commons Attribution Non-Commercial Share Alike license.
*
* Usage :
* const gg = new Garg(shadow, texture, bgColor);
* gg.setPalette(cPalette);
* const mask = gg.createMask(maskID, maskSize);
* image(mask, 0, 0);
* mask.remove();
*
* Change log :
* 2021.04.Dec
* Add remove procedure to internal graphics. Add remove() method to the Garg class.
* 2021.16.Oct
* Forked from sketch.js of 2021.Aug.27 version on https://github.com/Generativemasks/generativemasks.github.io
* The author of the original Generativemasks is Shunsuke Takawo (https://generativemasks.on.fleek.co/).
*
*/
class Garg {
inst; // this instance
cSize; // canvas size
sRadius; // mask shape radius
needShadow; // apply shadow or not
needTexture; // apply texture or not
needBackdrop; // paint background or not
palette; // selected palette
constructor(_shadow, _texture, _back) {
// define this instance canvas
this.inst = createGraphics(0, 0);
this.inst.colorMode(RGB, 255);
// it must draw a mask on 1600x1600 canvas
this.cSize = 1600;
const offset = this.cSize / 10;
this.sRadius = (this.cSize - offset * 2) * 3 / 4;
// switches
this.needShadow = _shadow;
this.needTexture = _texture;
this.needBackdrop = _back;
// set random palette
this.setRandomPalette();
}
/*
* setPalette : sets color palette from "rrggbb-rrggbb-rrggbb-rrggbb-rrggbb" style parameter.
*/
setPalette(_rgbStrings) {
const rgbStr = _rgbStrings.replace(/.*\//g, ""); // can use coolors url directly
if (this.chkRgbStrings(rgbStr)) {
this.palette = this.getPalette(rgbStr);
} else {
// if _rgbStrings was invalid set random palette
this.setRandomPalette();
}
}
/*
* setRandomPalette : sets random color palette.
*/
setRandomPalette() {
const defaultRGBs = [
"202c39-283845-b8b08d-f2d492-f29559",
"1f2041-4b3f72-ffc857-119da4-19647e",
"2f4858-33658a-86bbd8-f6ae2d-f26419",
"ffac81-ff928b-fec3a6-efe9ae-cdeac0",
"f79256-fbd1a2-7dcfb6-00b2ca-1d4e89",
"e27396-ea9ab2-efcfe3-eaf2d7-b3dee2",
"966b9d-c98686-f2b880-fff4ec-e7cfbc",
"50514f-f25f5c-ffe066-247ba0-70c1b3",
"177e89-084c61-db3a34-ffc857-323031",
"390099-9e0059-ff0054-ff5400-ffbd00",
"0d3b66-faf0ca-f4d35e-ee964b-f95738",
"177e89-084c61-db3a34-ffc857-323031",
"780000-c1121f-fdf0d5-003049-669bbc",
"eae4e9-fff1e6-fde2e4-fad2e1-e2ece9-bee1e6-f0efeb-dfe7fd-cddafd",
"f94144-f3722c-f8961e-f9c74f-90be6d-43aa8b-577590",
"555b6e-89b0ae-bee3db-faf9f9-ffd6ba",
"9b5de5-f15bb5-fee440-00bbf9-00f5d4",
"ef476f-ffd166-06d6a0-118ab2-073b4c",
"006466-065a60-0b525b-144552-1b3a4b-212f45-272640-312244-3e1f47-4d194d",
"f94144-f3722c-f8961e-f9844a-f9c74f-90be6d-43aa8b-4d908e-577590-277da1",
"f6bd60-f7ede2-f5cac3-84a59d-f28482",
"0081a7-00afb9-fdfcdc-fed9b7-f07167",
"f4f1de-e07a5f-3d405b-81b29a-f2cc8f",
"50514f-f25f5c-ffe066-247ba0-70c1b3",
"001219-005f73-0a9396-94d2bd-e9d8a6-ee9b00-ca6702-bb3e03-ae2012-9b2226",
"ef476f-ffd166-06d6a0-118ab2-073b4c",
"fec5bb-fcd5ce-fae1dd-f8edeb-e8e8e4-d8e2dc-ece4db-ffe5d9-ffd7ba-fec89a",
"e63946-f1faee-a8dadc-457b9d-1d3557",
"264653-2a9d8f-e9c46a-f4a261-e76f51",
];
// this random must be free from setting the randomSeed
this.palette = this.getPalette(this.inst.random(defaultRGBs));
}
/*
* getPalette : returns color palette.
* rgbStrings must be "rrggbb-rrggbb-rrggbb" style, at least 3 colors.
*/
getPalette(_rgbStrings) {
const arr = _rgbStrings.split("-");
for (let i = 0; i < arr.length; i++) {
arr[i] = this.inst.color("#" + arr[i]);
}
// this shuffle must be free from setting the randomSeed
return this.inst.shuffle(arr, true);
}
/*
* chkRgbStrings : returns RGB strings check result.
* true : check OK, false : error
*/
chkRgbStrings(_rgbStrings) {
const rgbStr = _rgbStrings.replace(/.*\//g, ""); // can use coolors url directly
const checker = new RegExp(/^(([0-9]|[a-fA-F]){6}-){2,}([0-9]|[a-fA-F]){6}$/);
if (checker.test(rgbStr)) {
return true;
} else {
return false;
}
}
/*
* createMask : returns _size resized Generativemasks (ID = _id) on p5.Graphics.
*/
createMask(_id, _size) {
// set color palette
const c = this.palette[0];
const shifted = this.palette.shift(); // without this, affect pattern
// define mask canvas
const maskCv = createGraphics(_size, _size);
maskCv.pixelDensity(1);
maskCv.colorMode(RGB, 255);
maskCv.angleMode(DEGREES);
maskCv.clear(); // transparent background
maskCv.fill(c);
maskCv.stroke(c);
maskCv.strokeWeight(30);
maskCv.scale(_size / this.cSize); // resize
// set mask id
maskCv.randomSeed(_id);
maskCv.noiseSeed(_id);
// draw shape and pattern
const nScale = maskCv.random(60, 200); // must be 60 - 200
this.drawShape(this.cSize / 2, this.cSize / 2, this.sRadius, nScale, maskCv);
// repair palette
this.palette.push(shifted);
// return resized canvas
const sizedPg = createGraphics(_size, _size);
if (this.needBackdrop) {
sizedPg.background(this.inst.random(this.palette));
} else {
sizedPg.clear();
}
if (this.needShadow) {
sizedPg.drawingContext.shadowColor = this.inst.color(0, 128);
sizedPg.drawingContext.shadowBlur = _size / 20;
sizedPg.drawingContext.shadowOffsetY = _size / 40;
}
sizedPg.image(maskCv, 0, 0);
maskCv.remove();
if (this.needTexture) {
sizedPg.scale(_size / this.cSize); // resize
const texture = this.getTexture(this.cSize);
sizedPg.image(texture, 0, 0);
texture.remove();
}
return sizedPg;
}
/*
* drawShape : clip the shape on the center of p5.Graphics g.
*/
drawShape(cx, cy, r, nPhase, target) {
const vertexPV = new Array();
// calculate the shape
let minX = this.cSize;
let maxX = -this.cSize;
let minY = this.cSize;
let maxY = -this.cSize;
for (let angle = 0; angle < 360; angle += 1) {
let nr = map(target.noise(cx, cy, (angle - 180) / nPhase), 0, 1, (r * 1) / 8, r);
nr = constrain(nr, 0, this.cSize / 2);
let x = target.cos(angle) * nr;
let y = target.sin(angle) * nr;
vertexPV.push(createVector(x, y));
minX = min(minX, x);
maxX = max(maxX, x);
minY = min(minY, y);
maxY = max(maxY, y);
}
// draw shape on the center of canvas
const divX = lerp(minX, maxX, 0.5);
const divY = lerp(minY, maxY, 0.5);
target.push();
// target.translate(cx, cy, r);
target.translate(cx, cy);
target.rotate(90);
target.beginShape();
for (let p of vertexPV) {
vertex(p.x - divX, p.y - divY);
}
target.endShape(CLOSE);
target.pop();
// clip shape
target.drawingContext.clip();
this.drawGraphic(-divY, -divX, this.cSize, this.cSize, this.palette, target);
}
/*
* drawGraphic : draw graphics on p5.Graphics target.
*/
drawGraphic(x, y, w, h, colors, target) {
let g = createGraphics(w / 2, h);
g.angleMode(DEGREES);
g.translate(x, y);
let gx = 0;
let gy = 0;
let gxStep, gyStep;
if (target.random() > 0.5) {
while (gy < g.height) {
gyStep = target.random(g.height / 100, g.height / 5);
if (gy + gyStep > g.height || g.height - (gy + gyStep) < g.height / 20) {
gyStep = g.height - gy;
}
gx = 0;
while (gx < g.width) {
gxStep = gyStep;
if (gx + gxStep > g.width || g.width - (gx + gxStep) < g.width / 10) {
gxStep = g.width - gx;
}
// g.ellipse(gx+gxStep/2,gy+gyStep/2,gxStep,gyStep);
this.drawPattern(g, gx, gy, gxStep, gyStep, colors, target);
gx += gxStep;
}
gy += gyStep;
}
} else {
while (gx < g.width) {
gxStep = target.random(g.width / 100, g.width / 5);
if (gx + gxStep > g.width || g.width - (gx + gxStep) < g.width / 20) {
gxStep = g.width - gx;
}
gy = 0;
while (gy < g.height) {
gyStep = gxStep;
if (gy + gyStep > g.height || g.height - (gy + gyStep) < g.height / 10) {
gyStep = g.height - gy;
}
// g.ellipse(gx+gxStep/2,gy+gyStep/2,gxStep,gyStep);
this.drawPattern(g, gx, gy, gxStep, gyStep, colors, target);
gy += gyStep;
}
gx += gxStep;
}
}
target.push();
// target.translate(x + w / 2, y + h / 2);
target.translate(w / 2, h / 2);
target.imageMode(CENTER);
target.scale(1, 1);
target.image(g, -g.width / 2, 0);
target.scale(-1, 1);
target.image(g, -g.width / 2, 0);
target.pop();
g.remove();
}
/*
* drawPattern : draw patterns on p5.Graphics g.
*/
drawPattern(g, x, y, w, h, colors, target) {
let rotate_num = (int(target.random(4)) * 360) / 4;
g.push();
g.translate(x + w / 2, y + h / 2);
g.rotate(rotate_num);
if (rotate_num % 180 == 90) {
let tmp = w;
w = h;
h = tmp;
}
g.translate(-w / 2, -h / 2);
if (this.needShadow) {
g.drawingContext.shadowColor = this.inst.color(0, 84);
g.drawingContext.shadowBlur = max(w, h) / 5;
}
let sep = int(target.random(1, 6));
let c = -1,
pc = -1;
g.stroke(0, (20 / 100) * 255);
switch (int(target.random(8))) {
case 0:
for (let i = 1; i > 0; i -= 1 / sep) {
while (pc == c) {
c = target.random(colors);
}
pc = c;
g.push();
g.scale(i);
g.strokeWeight(1 / i);
g.fill(c);
g.arc(0, 0, w * 2, h * 2, 0, 90);
g.pop();
}
break;
case 1:
for (let i = 1; i > 0; i -= 1 / sep) {
while (pc == c) {
c = target.random(colors);
}
pc = c;
g.push();
g.fill(c);
g.push();
g.translate(w / 2, 0);
g.scale(i);
g.strokeWeight(1 / i);
g.arc(0, 0, w, h, 0, 180);
g.pop();
g.push();
g.translate(w / 2, h);
g.scale(i);
g.strokeWeight(1 / i);
g.arc(0, 0, w, h, 0 + 180, 180 + 180);
g.pop();
g.pop();
}
break;
case 2:
for (let i = 1; i > 0; i -= 1 / sep) {
while (pc == c) {
c = target.random(colors);
}
pc = c;
g.push();
g.fill(c);
g.push();
g.scale(i);
g.strokeWeight(1 / i);
g.arc(0, 0, w * sqrt(2), h * sqrt(2), 0, 90);
g.pop();
g.push();
g.translate(w, h);
g.scale(i);
g.strokeWeight(1 / i);
g.arc(0, 0, w * sqrt(2), h * sqrt(2), 0 + 180, 90 + 180);
g.pop();
g.pop();
}
break;
case 3:
for (let i = 1; i > 0; i -= 1 / sep) {
while (pc == c) {
c = target.random(colors);
}
pc = c;
g.push();
g.translate(w / 2, h / 2);
g.scale(i);
g.strokeWeight(1 / i);
g.fill(c);
g.ellipse(0, 0, w, h);
g.pop();
}
break;
case 4:
for (let i = 1; i > 0; i -= 1 / sep) {
while (pc == c) {
c = target.random(colors);
}
pc = c;
g.push();
g.scale(i);
g.strokeWeight(1 / i);
g.fill(c);
g.triangle(0, 0, w, 0, 0, h);
g.pop();
}
break;
case 5:
for (let i = 1; i > 0; i -= 1 / sep) {
while (pc == c) {
c = target.random(colors);
}
pc = c;
g.push();
g.fill(c);
g.push();
g.translate(w / 2, 0);
g.scale(i);
g.strokeWeight(1 / i);
g.triangle(-w / 2, 0, w / 2, 0, 0, h / 2);
g.pop();
g.push();
g.translate(w / 2, h);
g.scale(i);
g.strokeWeight(1 / i);
g.triangle(-w / 2, 0, w / 2, 0, 0, -h / 2);
g.pop();
g.pop();
}
break;
case 6:
for (let i = 1; i > 0; i -= 1 / sep) {
while (pc == c) {
c = target.random(colors);
}
pc = c;
g.push();
g.fill(c);
g.push();
g.scale(i);
g.strokeWeight(1 / i);
g.triangle(0, 0, w * sqrt(2), 0, 0, h * sqrt(2));
g.pop();
g.push();
g.translate(w, h);
g.scale(i);
g.strokeWeight(1 / i);
g.arc(0, 0, -w * sqrt(2), 0, 0, -h * sqrt(2));
g.pop();
g.pop();
}
break;
case 7:
for (let i = 1; i > 0; i -= 1 / sep) {
while (pc == c) {
c = target.random(colors);
}
pc = c;
g.push();
g.translate(w / 2, h / 2);
g.rotate(45);
g.scale(i);
g.strokeWeight(1 / i);
g.fill(c);
g.rectMode(CENTER);
g.square(0, 0, sqrt(sq(w) + sq(h)));
g.pop();
}
break;
}
g.pop();
}
/*
* getTexture : returns texture image on _size x _size p5.Graphics.
*/
getTexture(_size) {
const tex = createGraphics(_size, _size);
tex.colorMode(HSB, 360, 100, 100, 100);
tex.angleMode(DEGREES);
tex.strokeWeight(0.1);
for (let x = 0; x < _size; x += 20) {
for (let y = 0; y < _size; y += 20) {
let angle = tex.random(75, 105);
let d = _size / 3; //10;
tex.stroke(0, 0, 0, tex.random(20));
tex.line(
x + tex.cos(angle) * d,
y + tex.sin(angle) * d,
x + tex.cos(angle + 180) * d,
y + tex.sin(angle + 180) * d
);
}
}
return tex;
}
}
こちらは version 1.0 と 1.0a の diff です。Garg の派生物を作られていた場合、こちらをパッチとしてご利用ください。
4c4
< * Version : 0.1
---
> * Version : 0.1a
5a6
> * Revise : tetunori
11c12,14
< * image(gg.createMask(maskID, maskSize), 0, 0);
---
> * const mask = gg.createMask(maskID, maskSize);
> * image(mask, 0, 0);
> * mask.remove();
13a17,18
> * 2021.04.Dec
> * Add remove procedure to internal graphics. Add remove() method to the Garg class.
185a191
> maskCv.remove();
188c194,196
< sizedPg.image(this.getTexture(this.cSize), 0, 0);
---
> const texture = this.getTexture(this.cSize);
> sizedPg.image(texture, 0, 0);
> texture.remove();
190d197
< return sizedPg;
191a199
> return sizedPg;
298a307,308
>
> g.remove();
謝辞
Garg を使って何か作っていただけると、とても嬉しいです。バグなど見つかりましたら Twitter で教えていただけるとありがたいです。
中山 哲法(@tetunori_lego)さんから、メモリリーク問題を解決するパッチをご提供いただきました。ありがとうございます!
実は、JavaScript のモジュールを作るは初めてで、果たしてこの 'Garg' を「モジュール」と読んでよいのかも自信が無いところです。
最後に、Garg 作成の楽しい時間を私に与えてくださった、Generativemasks のコード公開にかかわった皆さん、オリジナルの Generativemasks 作者である takawo shunsuke (@takawo) さんに感謝申し上げます。ありがとうございました。