Learn Creative Coding (#14) - Bezier Curves and Organic Shapes
Straight lines are fine for grids and geometry. But creative coding lives in the space between precision and organic flow -- and that space is made of curves. Flowing paths, smooth blobs, hand-drawn-looking lines, rivers of color, calligraphic strokes. Bezier curves are how you get there.
If you've ever used the pen tool in Illustrator, Figma, or Inkscape, you've already used Beziers. Those draggable handles? Those are control points. The smooth curve that forms between anchor points? That's the Bezier equation doing its thing. Today we learn what's happening under the hood, and more importantly, how to use it creatively in code -- for shapes that ellipse() and rect() could never dream of.
Last episode we explored trig for circular and spiral patterns. Bezier curves are the complement to that -- where trig gives you mathematically precise geometry (circles, spirals, roses), Beziers give you freeform organic flow. Together they cover basically every kind of shape you'll ever need in creative coding. And the really cool part: you can combine them. Trig for the structure, Beziers for the organic detail. But I'm getting ahead of myself.
This is one of those episodes where the technique applies everywhere. Curves aren't just pretty -- they're how fonts work, how SVGs are encoded, how motion paths are defined in animation. Every time you see a smooth line on a screen, there's probably a Bezier somewhere underneath :-)
What's a Bezier curve anyway?
A Bezier curve is defined by anchor points (where the curve starts and ends) and control points (which pull the curve toward them without the curve actually touching them). Think of the control points as magnets bending a rubber band. Or gravity wells that warp the space the line travels through -- similar to the attraction fields we built for our particle system in episode 11, except the "particle" here is a mathematical point sliding along the curve.
The simplest version is a straight line -- two anchor points, no control points. Add one control point and the line bends into a smooth arc. Add two control points and you can make S-curves, loops, and complex organic shapes. The math is elegant, the results are beautiful, and once you understand the concept you'll see Beziers everywhere.
Pierre Bezier developed these curves in the 1960s for designing car bodies at Renault. Paul de Casteljau independently discovered the same math at Citroen around the same time. Two French car companies, same problem, same elegant solution. The automotive industry's gift to computer graphics. And now every font on your screen, every SVG icon, every vector logo -- all Bezier curves :-)
Quadratic Bezier: one control point
Three points total: start, one control point, end.
function setup() {
createCanvas(500, 400);
background(240);
noFill();
stroke(50);
strokeWeight(3);
// start, control, end
let sx = 50, sy = 350;
let cx = 250, cy = 50; // control point pulls curve upward
let ex = 450, ey = 350;
beginShape();
vertex(sx, sy);
quadraticVertex(cx, cy, ex, ey);
endShape();
// visualize the control point and scaffolding
stroke(255, 0, 0);
strokeWeight(1);
line(sx, sy, cx, cy);
line(cx, cy, ex, ey);
fill(255, 0, 0);
noStroke();
ellipse(cx, cy, 8, 8);
}
The curve starts at (50, 350), gets pulled toward (250, 50), and ends at (450, 350). It never actually reaches the control point -- it just bends toward it, like gravity. The red lines show the "scaffolding" connecting start-to-control and control-to-end. This scaffolding is what you'd see in a vector graphics editor -- the handles.
The amount of pull depends on where the control point sits. Move it far away and the curve bends dramatically. Move it close to the line between start and end, and the curve is almost straight. Put it off to one side and you get asymmetric bends. There's no substitute for playing with coordinates to build intuition here -- try moving cy from 50 to 200 to 350 and watch how the curve flattens out. Try shifting cx to 100 or 400 and see how the curve leans.
Cubic Bezier: two control points
Four points: start, control1, control2, end. This is the real workhorse of vector graphics.
function setup() {
createCanvas(500, 400);
background(240);
noFill();
stroke(50);
strokeWeight(3);
// start point
let sx = 50, sy = 300;
// two control points
let c1x = 150, c1y = 50;
let c2x = 350, c2y = 350;
// end point
let ex = 450, ey = 100;
beginShape();
vertex(sx, sy);
bezierVertex(c1x, c1y, c2x, c2y, ex, ey);
endShape();
// visualize control points
stroke(255, 0, 0, 120);
strokeWeight(1);
line(sx, sy, c1x, c1y);
line(ex, ey, c2x, c2y);
noStroke();
fill(255, 0, 0);
ellipse(c1x, c1y, 8, 8);
fill(0, 0, 255);
ellipse(c2x, c2y, 8, 8);
}
Two control points let the curve change direction mid-path, creating S-curves and loops that a single control point can never achieve. This is how fonts, logos, and vector graphics describe complex shapes -- it's literally the same math behind every letter you're reading right now. The letter S? A cubic Bezier. The curve of a lowercase a? Multiple cubic Beziers chained together.
The first control point influences the start of the curve (which direction it "leaves" the start point). The second control point influences the end (which direction it "arrives" at the end point). If both control points are on the same side, you get a simple arc. If they're on opposite sides, you get an S-curve. If you cross them over each other, you can even get a loop -- though loops are tricky and rarely what you want for practical work.
The math: sampling points along a curve
Understanding the math behind Beziers is optional for using them, but it unlocks a lot of creative power. Specifically, it lets you sample points along a curve at any position, which means you can place objects, particles, or text along any curved path.
The key is a parameter t that goes from 0 to 1. At t=0 you're at the start point. At t=1 you're at the end. Everything in between is a smooth blend along the curve.
A quadratic Bezier at parameter t:
function quadBezier(t, p0, p1, p2) {
let mt = 1 - t;
return mt * mt * p0 + 2 * mt * t * p1 + t * t * p2;
}
A cubic Bezier:
function cubicBezier(t, p0, p1, p2, p3) {
let mt = 1 - t;
return mt*mt*mt*p0 + 3*mt*mt*t*p1 + 3*mt*t*t*p2 + t*t*t*p3;
}
The weighting is what creates the curve. At low t values, the start point and first control point dominate. At high t values, the end point and last control point take over. The transition between them is smooth and continuous -- no sudden jumps, no discontinuities.
Knowing this lets you sample points along a curve at any resolution:
function setup() {
createCanvas(500, 400);
background(20);
// curve control points
let x0 = 50, y0 = 300;
let cx1 = 150, cy1 = 50;
let cx2 = 350, cy2 = 350;
let x1 = 450, y1 = 100;
// draw the curve as a faint line
noFill();
stroke(50);
strokeWeight(1);
beginShape();
vertex(x0, y0);
bezierVertex(cx1, cy1, cx2, cy2, x1, y1);
endShape();
// now sample 30 points along it
noStroke();
for (let i = 0; i <= 30; i++) {
let t = i / 30;
let x = cubicBezier(t, x0, cx1, cx2, x1);
let y = cubicBezier(t, y0, cy1, cy2, y1);
// size and color vary with t
let size = 3 + t * 8;
fill(100 + t * 155, 80, 255 - t * 155, 200);
ellipse(x, y, size, size);
}
}
30 dots placed along a cubic Bezier, growing in size and shifting from purple to orange as t goes from 0 to 1. The dots follow the curve exactly because we're evaluating the same Bezier equation at evenly spaced t values.
This is incredibly useful for animation -- move an object along a curve by interpolating t from 0 to 1 over time. The object follows the curve perfectly. You'll want this for smooth motion paths that feel natural and hand-crafted rather than robotic straight-line movement.
The parameter t is your creative handle on the curve. Map it to size, color, opacity -- anything. Elements near the start (low t) can be small and faint, while elements near the end (high t) are large and bright. Or oscillating. Or driven by noise from episode 12. The curve gives you the path, t gives you the progression, and mapping gives you the aesthetics.
Chaining curves: where it gets real
One curve is useful. A chain of curves is powerful. And this is where Beziers go from "neat math trick" to "I can draw anything."
The key insight: make the end of one curve the start of the next. If the control point before and after a shared anchor point are collinear (on the same line through the anchor), the transition is perfectly smooth. If they're at an angle, you get a visible corner -- which is sometimes what you want (think of the sharp tip of a heart shape).
This is exactly how the pen tool works in vector editors. Every path you've ever drawn in Illustrator is a chain of cubic Bezier segments with shared anchor points. Now you're doing the same thing in code.
function setup() {
createCanvas(600, 400);
background(20);
noFill();
stroke(100, 200, 255);
strokeWeight(2);
// a flowing path with alternating control/anchor points
// for quadratic: every odd point is a control, every even is on-curve
let points = [
{ x: 50, y: 200 }, // start (on curve)
{ x: 120, y: 50 }, // control
{ x: 200, y: 200 }, // on curve
{ x: 280, y: 350 }, // control
{ x: 360, y: 200 }, // on curve
{ x: 440, y: 80 }, // control
{ x: 550, y: 200 }, // on curve (end)
];
beginShape();
vertex(points[0].x, points[0].y);
for (let i = 1; i < points.length - 1; i += 2) {
quadraticVertex(
points[i].x, points[i].y,
points[i + 1].x, points[i + 1].y
);
}
endShape();
// show the scaffolding
stroke(255, 80, 80, 80);
strokeWeight(1);
for (let i = 0; i < points.length - 1; i++) {
line(points[i].x, points[i].y, points[i+1].x, points[i+1].y);
}
// mark control vs anchor points
noStroke();
for (let i = 0; i < points.length; i++) {
fill(i % 2 === 0 ? color(100, 200, 255) : color(255, 80, 80));
ellipse(points[i].x, points[i].y, i % 2 === 0 ? 8 : 6);
}
}
Three quadratic segments chained together, making a smooth wave. The blue dots are anchor points (on-curve), the red dots are control points (off-curve). Notice how the wave flows continuously -- the curve passes through every blue dot and gets pulled toward every red dot. Changing any single control point reshapes its local segment without affecting distant parts of the curve. That locality is what makes Beziers so practical for design.
Organic blobs: Beziers meet noise
Allez, here's where things get fun. In episode 13 we deformed circles with noise to make organic blobs using polar coordinates. We can do the same thing with a different approach -- using curve vertices for even smoother shapes:
function setup() {
createCanvas(500, 500);
background(20);
noStroke();
fill(100, 200, 150, 180);
drawBlob(250, 250, 120, 8);
}
function drawBlob(cx, cy, radius, segments) {
let points = [];
// generate points around a circle with noise displacement
for (let i = 0; i < segments; i++) {
let angle = (i / segments) * TWO_PI;
let r = radius + noise(cos(angle) + 1, sin(angle) + 1) * 60 - 30;
points.push({
x: cx + cos(angle) * r,
y: cy + sin(angle) * r
});
}
// draw smooth closed shape using curve vertices
// curveVertex uses Catmull-Rom splines -- passes THROUGH the points
beginShape();
for (let i = 0; i < points.length + 3; i++) {
let p = points[i % points.length];
curveVertex(p.x, p.y);
}
endShape(CLOSE);
}
curveVertex is p5.js's Catmull-Rom spline implementation -- it passes through the actual points, unlike Bezier control points which the curve doesn't touch. That's a key difference to remember: Bezier bends toward control points, Catmull-Rom goes through them. For organic blobs where you want the shape to hit specific coordinates, Catmull-Rom is usually easier to work with.
The noise sampling trick here is the same one we used in episode 13: cos(angle) and sin(angle) as noise inputs instead of the angle directly. This guarantees seamless closure because cos and sin are periodic -- they return to their starting values after a full rotation.
The + 3 overlap in the loop ensures the shape closes smoothly. Without it, there'd be a sharp corner where the last point meets the first. The extra iterations let the spline "wrap around" and maintain its curvature through the closing seam. It's a small detail but it makes the difference between a blob that looks organic and one that looks like a polygon with one awkward corner.
Layered blob compositions
Multiple blobs with different noise offsets, sizes, and colors:
function setup() {
createCanvas(600, 600);
background(15);
noStroke();
// layer 1: large, faint background blobs
for (let i = 0; i < 12; i++) {
let x = random(100, 500);
let y = random(100, 500);
let r = random(80, 160);
fill(random(180, 240), random(80, 130), random(120, 180), 30);
noiseSeed(i * 100);
drawBlob(x, y, r, 10);
}
// layer 2: medium, more saturated
for (let i = 0; i < 8; i++) {
let x = random(150, 450);
let y = random(150, 450);
let r = random(40, 90);
fill(random(150, 255), random(80, 150), random(100, 200), 60);
noiseSeed(i * 100 + 5000);
drawBlob(x, y, r, 12);
}
// layer 3: small bright accents
for (let i = 0; i < 15; i++) {
let x = random(100, 500);
let y = random(100, 500);
let r = random(15, 40);
fill(random(200, 255), random(150, 255), random(180, 255), 100);
noiseSeed(i * 100 + 9000);
drawBlob(x, y, r, 8);
}
}
Three layers of blobs: big faint ones in the back, medium saturated ones in the middle, small bright accents on top. Each layer uses different noiseSeed() values so every blob has a unique shape. The layering creates depth -- your eye reads the bright small blobs as "close" and the faint large ones as "far away."
This is one of those techniques that scales beautifully. Eight blobs is a sketch. Eighty blobs is art. Eight hundred blobs at low opacity starts to look like abstract oil painting. The organic shapes overlap and blend in ways that regular geometry never can. I've seen generative artists sell prints of nothing but layered noise blobs with carefully chosen color palettes -- and they're genuinely beautiful. The color choices make or break it. Complementary colors at low opacity create rich, dark overlaps. Analogous colors create soft, harmonious blends. Try a palette of warm pinks and oranges, or cool blues and teals.
Flow fields with curves
Remember the noise we built from scratch in episode 12? And the flow fields I teased? Here's where curves make flow fields go from "interesting" to "iconic." Instead of short line segments, draw long, flowing curves that follow the noise field:
function setup() {
createCanvas(600, 600);
background(15);
let steps = 80;
let stepSize = 3;
for (let i = 0; i < 500; i++) {
let x = random(width);
let y = random(height);
stroke(random(150, 255), random(80, 150), random(180, 255), 40);
strokeWeight(0.8);
noFill();
beginShape();
for (let s = 0; s < steps; s++) {
curveVertex(x, y);
let angle = noise(x * 0.003, y * 0.003) * TWO_PI * 2;
x += cos(angle) * stepSize;
y += sin(angle) * stepSize;
// stop if out of bounds
if (x < 0 || x > width || y < 0 || y > height) break;
}
endShape();
}
}
500 curves, each following the noise field. They cluster and diverge, creating rivers and eddies. This is one of the most iconic generative art techniques in existence. Tyler Hobbs's Fidenza (one of the most valuable NFT collections ever, individual pieces selling for hundreds of thousands of dollars) is essentially a sophisticated flow field with carefully chosen colors and stroke weights.
The beauty of flow fields is emergence. Each individual curve follows a simple rule: "look at the noise value at my current position, turn to face that direction, step forward." But 500 of them following the same field create complex, river-like patterns that nobody explicitly designed. The patterns emerge from the interaction of simple rules with the noise landscape. That's generative art in a nutshell -- simple rules, complex results.
Notice I used curveVertex instead of vertex here. With vertex, each step creates a sharp angle change. With curveVertex, the spline smooths through the points, making the curves look like they were drawn with a pen rather than plotted point by point. The difference is subtle at high step counts but very visible at lower step counts.
The key parameters to play with:
- Noise scale (0.003): smaller = bigger, smoother flows. Larger = tight, chaotic swirls
- Steps: how long each curve runs. More steps = longer, more flowing lines
- Step size: how far each step moves. Smaller = smoother curves, larger = more angular
- Number of curves: density of the field. Too few looks sparse, too many becomes mud
- Color mapping: use the curve's starting position, length, or direction to drive color. Curves starting on the left could be warm, curves starting on the right could be cool. Spatial color mapping adds visual coherence to what could otherwise look random
- Stroke weight variation: vary weight along the curve for calligraphic effects. Thinner at the start, thicker in the middle, thin again at the end -- like a brush stroke
Animated Bezier curves
Static curves are nice, but animation is where curves really shine. Use frameCount to move control points over time and the curves breathe, writhe, and dance:
let points = [];
function setup() {
createCanvas(500, 500);
// create 6 control points that will be animated
for (let i = 0; i < 6; i++) {
points.push({
baseX: 80 + i * 70,
baseY: 250,
freqX: random(0.01, 0.03),
freqY: random(0.01, 0.04),
ampX: random(20, 60),
ampY: random(40, 100),
phase: random(TWO_PI)
});
}
}
function draw() {
background(15, 25); // trail effect
let t = frameCount * 0.02;
// calculate animated positions
let animated = points.map(p => ({
x: p.baseX + sin(t * p.freqX * 60 + p.phase) * p.ampX,
y: p.baseY + sin(t * p.freqY * 60 + p.phase * 1.3) * p.ampY
}));
// draw the flowing curve
noFill();
strokeWeight(2);
for (let layer = 0; layer < 3; layer++) {
let offset = layer * 0.15;
stroke(
100 + layer * 60,
150 - layer * 30,
255 - layer * 40,
120 - layer * 20
);
beginShape();
for (let i = 0; i < animated.length; i++) {
curveVertex(
animated[i].x + sin(t + i + offset) * 10,
animated[i].y + cos(t + i * 0.7 + offset) * 10
);
}
endShape();
}
}
Six control points, each oscillating with different frequencies, amplitudes, and phases -- exactly the phase offset technique we learned last episode. Three slightly-offset copies of the curve create a ribbon-like effect. The low-alpha background creates ghostly trails. The result looks like a living, breathing organism.
The phase offsets (p.phase and p.phase * 1.3) are critical here. Without them, all points would oscillate in sync and the curve would just bounce up and down rigidly. With different phases, each point reaches its peak at a different time, creating wave-like motion that propagates through the curve. Same principle as episode 13's animated spiral, but applied to a freeform curve instead of polar coordinates.
Drawing a smooth curve through mouse positions
Here's something pratcial: collect mouse positions over time and draw a smooth curve through them. This is how drawing apps work -- the raw mouse input is just a series of points, and a spline smooths them into a natural-looking line:
let trail = [];
const MAX_TRAIL = 80;
function setup() {
createCanvas(600, 600);
background(20);
}
function draw() {
background(20, 15);
// add current mouse position to trail
if (mouseIsPressed) {
trail.push({ x: mouseX, y: mouseY });
if (trail.length > MAX_TRAIL) trail.shift();
}
// draw the smooth curve through all trail points
if (trail.length > 3) {
noFill();
strokeWeight(3);
beginShape();
for (let i = 0; i < trail.length; i++) {
// color fades from tail to head
let t = i / trail.length;
stroke(
100 + t * 155,
50 + t * 100,
255,
t * 255
);
curveVertex(trail[i].x, trail[i].y);
}
endShape();
// draw dots at each sample point
noStroke();
for (let i = 0; i < trail.length; i++) {
let t = i / trail.length;
fill(255, 200 + t * 55, 100, t * 200);
ellipse(trail[i].x, trail[i].y, 2 + t * 4);
}
}
}
Click and drag to draw. The curveVertex call smooths the mouse positions into a flowing line that looks hand-drawn rather than jagged. The color gradient from faint-at-the-tail to bright-at-the-head gives the line a sense of direction, like it's being drawn in real time. The small dots at each sample point show where the raw mouse data is -- you can see how the curve smoothly interpolates between them.
This is the foundation of digital drawing tools. Procreate, Krita, Photoshop -- they all do some version of this. The smoothing algorithm varies (some use cubic Bezier fitting, some use Catmull-Rom, some use more sophisticated B-splines), but the core idea is the same: take noisy input points, produce a smooth curve.
Vanilla Canvas Beziers
For completeness, here's how all of this works without p5, using the raw Canvas API we've been building on since episode 9:
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
canvas.width = 500;
canvas.height = 400;
ctx.fillStyle = '#111';
ctx.fillRect(0, 0, 500, 400);
// quadratic bezier
ctx.beginPath();
ctx.moveTo(50, 300);
ctx.quadraticCurveTo(250, 50, 450, 300);
ctx.strokeStyle = 'rgba(100, 200, 255, 0.8)';
ctx.lineWidth = 3;
ctx.stroke();
// cubic bezier
ctx.beginPath();
ctx.moveTo(50, 350);
ctx.bezierCurveTo(150, 100, 350, 350, 450, 150);
ctx.strokeStyle = 'rgba(255, 100, 150, 0.8)';
ctx.lineWidth = 3;
ctx.stroke();
Same concepts, slightly different function names. quadraticCurveTo and bezierCurveTo instead of p5's quadraticVertex and bezierVertex. If you're working without p5 (as we've been doing since episode 9 went vanilla), these are your tools.
One thing the Canvas API does that p5 doesn't expose directly: Path2D objects. You can create a path once and reuse it at different positions, scales, and colors. If you're drawing the same curve hundreds of times, creating a Path2D once and drawing it repeatedly is significantly faster than rebuilding the path each frame:
// create path once
const wavePath = new Path2D();
wavePath.moveTo(0, 0);
wavePath.bezierCurveTo(30, -40, 70, 40, 100, 0);
// draw it many times at different positions
for (let i = 0; i < 50; i++) {
ctx.save();
ctx.translate(10 + i * 11, 200 + Math.sin(i * 0.3) * 80);
ctx.rotate(i * 0.1);
ctx.strokeStyle = `hsl(${i * 7}, 70%, 60%)`;
ctx.lineWidth = 1.5;
ctx.stroke(wavePath);
ctx.restore();
}
50 copies of the same Bezier wave, each translated, rotated, and colored differently. The path is defined once and the GPU handles the rest. This matters when you're pushing thousands of curves per frame -- Path2D plus save()/restore() for transform state is the performant way to do it.
Bezier vs Catmull-Rom: when to use which
This comes up a lot and the answer is pretty simple:
Catmull-Rom (curveVertex) -- use when you want the curve to pass through specific points. Great for: blobs, mouse trails, connecting data points, any situation where you have coordinates and you want a smooth line through all of them.
Bezier (bezierVertex, quadraticVertex) -- use when you want precise control over the curve's shape between two endpoints. Great for: logo design, font creation, precise paths, animation easing curves, any situation where you need to control the exact curvature.
In practice, Catmull-Rom is what you reach for 80% of the time in creative coding. It's simpler (no separate control points to manage) and it does what you usually want (smooth line through a set of points). Bezier is for when you need surgical precision over the curve shape -- or when you're interoperating with SVG, font formats, or vector tools that all speak Bezier natively.
You can actually convert between them mathematically, but that's a rabbit hole for another day. For now, just know both exist and reach for whichever one matches your problem.
Putting it all together: a curve-based composition
Let's combine flow field curves with organic blobs and Bezier sampling into a single piece you could print and hang on a wall:
function setup() {
createCanvas(600, 600);
background(12);
colorMode(HSB, 360, 100, 100, 100);
// layer 1: soft background blobs
noStroke();
for (let i = 0; i < 10; i++) {
fill(200 + i * 12, 40, 70, 15);
noiseSeed(i * 77);
drawBlob(random(100, 500), random(100, 500), random(60, 140), 10);
}
// layer 2: flow field curves
for (let i = 0; i < 300; i++) {
let sx = random(width);
let sy = random(height);
let hue = map(sx, 0, width, 180, 300);
stroke(hue, 60, 80, 25);
strokeWeight(random(0.5, 1.5));
noFill();
beginShape();
let x = sx, y = sy;
for (let s = 0; s < 60; s++) {
curveVertex(x, y);
let angle = noise(x * 0.004, y * 0.004) * TWO_PI * 2;
x += cos(angle) * 3;
y += sin(angle) * 3;
if (x < 0 || x > width || y < 0 || y > height) break;
}
endShape();
}
// layer 3: bezier accent curves
noFill();
for (let i = 0; i < 20; i++) {
let x0 = random(width);
let y0 = random(height);
let x1 = x0 + random(-200, 200);
let y1 = y0 + random(-200, 200);
let cx1 = (x0 + x1) / 2 + random(-100, 100);
let cy1 = (y0 + y1) / 2 + random(-150, 150);
let cx2 = (x0 + x1) / 2 + random(-100, 100);
let cy2 = (y0 + y1) / 2 + random(-150, 150);
stroke(30 + random(30), 70, 90, 40);
strokeWeight(random(1, 3));
beginShape();
vertex(x0, y0);
bezierVertex(cx1, cy1, cx2, cy2, x1, y1);
endShape();
}
// layer 4: scattered dots along a hidden bezier path
noStroke();
for (let curve = 0; curve < 5; curve++) {
let ax = random(100, 500), ay = random(100, 500);
let bx = random(100, 500), by = random(100, 500);
let c1x = random(width), c1y = random(height);
let c2x = random(width), c2y = random(height);
for (let i = 0; i <= 40; i++) {
let t = i / 40;
let x = cubicBezier(t, ax, c1x, c2x, bx);
let y = cubicBezier(t, ay, c1y, c2y, by);
fill(40 + t * 20, 80, 90, 60 + t * 30);
ellipse(x, y, 2 + t * 5);
}
}
}
Four layers: noise blobs for atmospheric depth, flow field curves for the main visual texture, random cubic Beziers for accent lines, and sampled dots along hidden Bezier paths for sparkle and detail. Every technique from this episode working together. Change the random seed and you get a completely different composition with the same visual DNA.
This kind of layered generative composition is exactly what I mean when I say curves are a foundational creative coding tool. You don't just draw one curve -- you draw hundreds, at different scales and opacities, and the layering creates something richer than any single element could achieve.
't Komt erop neer...
- Bezier curves use control points as "magnets" that pull the curve without the curve actually touching them
- Quadratic Bezier = 1 control point, cubic Bezier = 2 control points -- cubic is the workhorse of all vector graphics
- The math: sample any point on a curve with parameter
t(0 to 1) -- use this to place objects, particles, or color along curves - Chain Beziers by sharing anchor points -- keep control points collinear for smooth transitions
curveVertexcreates Catmull-Rom splines that pass through the points (great for blobs, mouse trails)- Organic blobs = polar coords + noise radius + curve vertices -- same seamless-closure trick from episode 13
- Flow fields with long
curveVertexcurves = the iconic generative art technique (Fidenza, etc.) - Canvas API equivalents:
quadraticCurveTo,bezierCurveTo,Path2Dfor reuse and performance - Catmull-Rom for "smooth line through points," Bezier for "precise control over curve shape"
- The same Bezier math powers fonts, SVGs, and every vector graphics tool
Curves are one of those techniques where the gap between "I understand the concept" and "I can make beautiful things with it" is pretty small. The math is simple, the creative possibilities are deep, and the results look immediately impressive. Spend some time playing -- draw random flow fields, generate blob compositions, chain curves with different control points. The more you experiment, the more your intuitions about what makes a curve "feel right" will develop. Those intuitions are what separate good generative art from great generative art.
We've now got trig for geometric precision and Beziers for organic flow. Next time we combine everything we've built so far -- particles, trig, noise, and curves -- into an interactive project that puts it all together. Should be fun :-)
Sallukes! Thanks for reading.
X