the Beauty of Generative Watercolor Art: A Blend of Code and Color
written by reyrove
The Canvas Setup: Creating the Space for Art
The first step in our artistic journey is setting up the canvas and defining our workspace.
const e = Math.min(innerWidth, innerHeight);
const canvas = { w: e, h: e };
- Dynamic Canvas Sizing : We create a square canvas based on the smaller of the browser’s width and height to ensure that our artwork scales nicely across devices.
Next, we initialize arrays for the brushstrokes, colors, and dreamy elements. We also define variables that control the number of brushstrokes and elements in the artwork.
let brushColors = [];
let brushStrokes = [];
let paperTexture;
let dreamyElements = [];
let backgroundColors = [];
const numBrushStrokes = 60;
const numColors = 30;
const numDreamyElements = 20;
These variables will help us populate the artwork with the right number of elements and variations.
Color Initialization: Building the Palette
The watercolor effect relies heavily on a soft, harmonious palette. To achieve this, we generate random colors within a pleasant range.
function initializeColors() {
for (let i = 0; i < numColors; i++) {
brushColors.push(color($fx.rand() * 205 + 50, $fx.rand() * 205 + 50, $fx.rand() * 205 + 50, $fx.rand() * 100 + 50));
}
}
- Randomized Colors: Colors are generated using random values, ensuring they fall within a soft spectrum of RGB values (between 50 and 255). The added transparency (alpha) provides the delicate watercolor effect.
Creating the Background: Subtle Layering
A good watercolor piece often begins with a soft gradient or layered background. Here, we divide the canvas into segments, each filled with a different color.
function createBackground() {
for (let i = 0; i < numColors; i++) {
backgroundColors.push(color($fx.rand() * 205 + 50, $fx.rand() * 205 + 50, $fx.rand() * 205 + 50, 255));
}
}
In the drawBackground
function, we apply these colors in rectangular segments.
function drawBackground() {
let segmentHeight = canvas.h / numColors;
for (let i = 0; i < numColors; i++) {
fill(backgroundColors[i]);
rect(0, i * segmentHeight, canvas.w, segmentHeight);
}
}
- Background Segments: By splitting the canvas into vertical strips of color, we create a soft, blended background that sets the tone for the rest of the artwork.
Brushstrokes: The Heart of Watercolor
Next, we simulate brushstrokes by placing large, semi-transparent ellipses with feathered edges.
function createBrushStrokes() {
for (let i = 0; i < numBrushStrokes; i++) {
let x = $fx.rand() * canvas.w;
let y = $fx.rand() * canvas.h;
let size = $fx.rand() * (canvas.w / 3 - canvas.w / 8) + canvas.w / 8;
let colorIndex = floor($fx.rand() * brushColors.length);
brushStrokes.push({
x: x,
y: y,
color: brushColors[colorIndex],
size: size
});
}
}
In drawWatercolorStroke
, we draw the brushstrokes with layered ellipses, gradually decreasing in size and transparency to mimic the real-life effects of watercolor.
function drawWatercolorStroke(x, y, col, size) {
push();
translate(x, y);
rotate($fx.rand() * TWO_PI);
noStroke();
let baseAlpha = col._getAlpha();
for (let i = 0; i < 7; i++) {
fill(col);
ellipse(0, 0, size * ($fx.rand() * 0.2 + 0.9), size * ($fx.rand() * 0.6 + 0.7));
size *= 0.85;
col.setAlpha(baseAlpha * (1 - i / 7));
}
drawingContext.filter = 'blur(2px)';
ellipse(0, 0, size, size);
drawingContext.filter = 'none';
drawInkBleed(x, y, col, size * 1.5);
pop();
}
- Soft Transitions & Blurring : To capture the softness of watercolor, we apply multiple layers with gradually reducing opacity and size, combined with a blur effect for the edges.
- Ink Bleed : Real watercolor often bleeds into the paper. We simulate this by drawing additional faded ellipses around the stroke to mimic this effect.
Dreamy Elements: Adding Ethereal Features
In this artwork, we add glowing orbs to evoke a dreamy, magical atmosphere. These orbs are softly colored and have glowing edges.
function createDreamyElements() {
for (let i = 0; i < numDreamyElements; i++) {
let x = $fx.rand() * canvas.w;
let y = $fx.rand() * canvas.h;
let size = $fx.rand() * (canvas.w / 5 - canvas.w / 10) + canvas.w / 10;
let col = color($fx.rand() * 105 + 150, $fx.rand() * 105 + 150, $fx.rand() * 105 + 150, $fx.rand() * 50 + 50);
let glow = $fx.rand() * 20 + 10;
dreamyElements.push({ x, y, size, col, glow });
}
}
function drawGlowingOrb(x, y, size, col, glow) {
push();
translate(x, y);
drawingContext.shadowBlur = glow;
drawingContext.shadowColor = col;
noStroke();
fill(col);
ellipse(0, 0, size, size);
drawingContext.shadowBlur = 0;
pop();
}
- Glowing Orbs : These ethereal features add a magical touch, with glow effects that make them seem like they are floating over the brushstrokes.
Finishing Touches: Simulating Paper Texture
Lastly, we apply a subtle paper texture to give the digital artwork a real-world feel.
function createPaperTexture() {
paperTexture = createGraphics(canvas.w, canvas.h);
paperTexture.noiseDetail(2, 0.5);
for (let x = 0; x < canvas.w; x++) {
for (let y = 0; y < canvas.h; y++) {
let alpha = paperTexture.noise(x * 0.02, y * 0.02) * 50 + 100;
paperTexture.stroke(255, alpha);
paperTexture.point(x, y);
}
}
}
By blending the paper texture on top of the artwork, we mimic the natural graininess of real watercolor paper.
function applyPaperTexture() {
blendMode(OVERLAY);
image(paperTexture, 0, 0, canvas.w, canvas.h);
blendMode(BLEND);
}
Final Thoughts
This code allows us to generate watercolor-inspired generative art, blending algorithmic precision with the randomness and fluidity of nature. The soft transitions, ethereal elements, and textured background come together to create a piece that feels both digitally crafted and artistically expressive. for full code, click here.
Try it yourself: Load the code into your editor, experiment with different brushstrokes, colors, and dreamy elements, and see how your unique piece of generative art unfolds!
With flowing hues, gentle strokes, and boundless imagination by Frostbond Coders 🎨✨