Meet 'Garg': A p5.js Library for Customizing Generativemasks

Generativemasks example image.

Crafting My Own 'Generativemasks' Generator

'Generativemasks' is a fascinating NFT art project that produces unique patterns and colorways every time you refresh.

The project is open-source under CC BY-NC-SA 3.0. And I was so captivated by the project that I decided to dive in and create my own derivative.

[Japanese version / 日本語版はこちら]


While exploring the original code, I noticed a few technical hurdles for creating derivatives:

  • Mask shapes changed depending on the canvas size.
  • randomSeed() calls affected the global environment.

To address these, I developed a JavaScript module that returns the mask as a p5.Graphics object. Now, you can easily define your own sizes and color palettes!

It solves the quirks of the original implementation, making it much easier to build upon.

Let me show you this JavaScript module and the example application. It must be fun to make some derivatives with this.

 

Why I Built Garg: Flexibility and Freedom

I developed a module that allows you to generate 'Generativemasks' with the following features:

  • Adjustable Size: Generate masks at any resolution.
  • Custom Color Palettes: Compatible with Coolors URL formats.
  • Background Options: Toggle between solid colors or transparency.
  • Visual Effects: Toggleable shadows and textures.

The name of this module is 'Garg'. I named this module 'Garg'—an acronym for 'Generates A Resembling Generativemasks'.

'Garg' can generate much the same as Generativemasks shape and pattern. But they are not identical because I modified the mask generation logic of the original code.

You can use the original code if you want the original shape.

 

The example application.

I made a web application called 'Generativemasks to Your Taste' to demonstrate the functions of 'Garg'. You can make your favorite size and color mask with it.

The appearance of web application called 'Generativemasks to Your Taste'.
'Generativemasks to Your Taste' on OpenProcessing.

The settings below will generate the mask much the same as the original Generativemasks.

  • Size = 1600
  • Color palette : none
  • Background color : on
  • Shadow : on
  • Texture : on

 

Application Gallery

Garg Meets p5.pattern

I also collaborated with p5.pattern, a brilliant library by SYM (@hyappy717). By tweaking Garg to work with p5.pattern, I created 'GargPattern'—opening up even more visual possibilities.

The appearance of web application called 'Generativemasks meets p5.pattern'.
'Generativemasks meets p5.pattern' on OpenProcessing.

 

Generativemasks Abstractor

It makes Generativemasks to the abstract art. I applied the Node-Garden technique in the article 'The node-garden technique : application section.' to the image of Generativemasks.

It's just the reason to make the 'Garg'

The appearance of web application called 'Generativemasks Abstractor'.
'Generativemasks Abstractor' on OpenProcessing.

You'll get an image like this with low-level abstraction.

The example result image of web application called 'Generativemasks Abstractor'.

 

Getting Started with Garg

Here’s a quick snippet to get a mask onto your canvas. This example generates Mask #37 at 480x480px with shadows enabled, no texture effect and no background-color:


const gg = new Garg(true, false, false);
const mask = gg.createMask(37, 480);
image(mask, 0, 0);
mask.remove();

The example mask image that was generated by 'Garg'.

 

Constructor

new Garg(s, t, b)

Parameters

s boolean: true Shadow effect on
t boolean: true Texture effect on
b boolean: true Background-color on, false Tranaparent background

 

Check the Color Palette

Method

chkRgbStrings(s)

Parameters

s String: Color palette

Description

It checks the syntax of the color palette. It returns true if the syntax is valid; otherwise, it returns false.

The color palette must be RGB, 'RRGGBB-RRGGBB-RRGGBB' form and it needs at least three colors. For your bonus, it is compatible with the 'coolers' URL form.
ex. https://coolors.co/000000-14213d-fca311-e5e5e5-ffffff

 

Create the Mask

Method

createMask(i, s)

Parameters

i Number: Generativemasks ID to create. The original id is between 0 to 9999. 'Garg' accepts over 10000.
s Number: The size of the image to create. It returns a square image so it is s x s pixels image.

Description

It create mask and it returns as 'p5.Graphics'. Please 'remove() properly the 'p5.Graphics' when it becomes unneeded'.

 

Apply Color Palette

Method

setPalette(p)

Parameters

p String: Color palette

Description

It applies the given color palette. To make the mask with this color palette, you need to run this method before 'createMask()'.

See 'chkRgbStrings()' description for the syntax of the color palette.

 

Apply Random Color Palette

Method

setRandomPalette()

Parameters

none

Description

It applies the randomly chosen color palette from the palettes inside 'Garg'

 

The Code of 'Garg'.

The latest version 1.0a is here. The code is collapsed below due to its length.


/* 
 * 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;
    }

}


 

Fixing the Memory Leak Bug

In Garg 1.0, I missed removing unused graphics objects, which caused massive memory leaks.

Check out my other post on why calling remove() is vital for your browser's health!

Below is the diff between version 1.0 and 1.0a. Please apply this patch to your Garg 1.0-based projects.


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();

 

Acknowledgement

I've had a fun time making 'Garg'. I would like to thank you all for your effort to publish the Generativemasks code and the originator of Generativemasks takawo shunsuke (@takawo).

It's my great pleasure to see you make something with 'Garg'. If you find bugs or something please let me know via Twitter.

Mr. Tetsunori NAKAYAMA (@tetunori_lego) kindly send me a patch to fix the memory leak problem. Thank you Nakayama san!

Honestly, this was my first time writing a JS module—I hope it actually qualifies as one!

 

Next Post Previous Post
No Comment
Add Comment
comment url