Paint Splatter Art: How to Create Stunning Digital Splatter Effects With Code
There is something primal about flinging paint. Jackson Pollock knew it when he laid his canvases on the floor and dripped, poured, and splashed enamel in sweeping arcs. Hans Namuth’s photographs of Pollock at work became icons of 20th-century art—the painter as athlete, the studio as arena. Paint splatter art has since evolved far beyond its Abstract Expressionist origins, inspiring street artists, designers, and now a new generation of creative coders who simulate the physics of flung pigment on a digital canvas.
What makes paint splatter so visually compelling? The answer lies in physics. When a drop of paint strikes a surface, it spreads under momentum, fragments into sub-droplets due to surface tension, and leaves satellite spots that trail away from the impact. Viscosity determines whether the paint pools thickly or shatters into fine mist. Gravity pulls drips downward in long streaks. These forces—velocity, viscosity, surface tension, gravity—are all quantities we can model in code. The HTML5 Canvas API gives us the pixel-level control to render every droplet, drip, and tendril.
In this guide you will build eight complete paint splatter simulations, each exploring a different aspect of the physics and aesthetics of flung paint. Every example is a standalone HTML file—copy it into your browser and start splattering immediately.
1. Basic Paint Splatter
We begin with the fundamental mechanic: click to splatter. Each click spawns a cluster of sub-droplets that radiate outward from the impact point at random angles and velocities. Larger droplets stay close to the center; smaller ones fly farther. Each droplet lands as a slightly elongated circle whose stretch direction follows its velocity vector, just like real paint splattering on a flat surface.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Basic Paint Splatter</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { background: #f5f0e8; overflow: hidden; cursor: crosshair; }
canvas { display: block; }
.info {
position: fixed; bottom: 16px; left: 50%; transform: translateX(-50%);
font-family: sans-serif; font-size: 14px; color: #666;
background: rgba(255,255,255,0.8); padding: 6px 14px; border-radius: 20px;
}
</style>
</head>
<body>
<canvas id="c"></canvas>
<div class="info">Click anywhere to splatter paint</div>
<script>
var canvas = document.getElementById('c');
var ctx = canvas.getContext('2d');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
ctx.fillStyle = '#f5f0e8';
ctx.fillRect(0, 0, canvas.width, canvas.height);
var palette = [
'#e63946', '#457b9d', '#2a9d8f', '#e9c46a',
'#264653', '#f4a261', '#6a0572', '#1d3557'
];
function splatter(x, y) {
var color = palette[Math.floor(Math.random() * palette.length)];
var count = 30 + Math.floor(Math.random() * 40);
for (var i = 0; i < count; i++) {
var angle = Math.random() * Math.PI * 2;
var speed = Math.random() * Math.random() * 120;
var dx = Math.cos(angle) * speed;
var dy = Math.sin(angle) * speed;
var radius = 2 + Math.random() * 8 * (1 - speed / 120);
var px = x + dx;
var py = y + dy;
var stretch = 1 + speed / 60;
ctx.save();
ctx.translate(px, py);
ctx.rotate(angle);
ctx.scale(stretch, 1);
ctx.beginPath();
ctx.arc(0, 0, radius, 0, Math.PI * 2);
ctx.fillStyle = color;
ctx.globalAlpha = 0.6 + Math.random() * 0.4;
ctx.fill();
ctx.restore();
}
// central pool
ctx.beginPath();
ctx.arc(x, y, 8 + Math.random() * 12, 0, Math.PI * 2);
ctx.fillStyle = color;
ctx.globalAlpha = 0.8;
ctx.fill();
ctx.globalAlpha = 1;
}
canvas.addEventListener('click', function(e) {
splatter(e.clientX, e.clientY);
});
window.addEventListener('resize', function() {
var img = ctx.getImageData(0, 0, canvas.width, canvas.height);
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
ctx.putImageData(img, 0, 0);
});
</script>
</body>
</html>
The key insight is that droplet radius and travel distance are inversely related. In real paint physics, smaller droplets have less mass and therefore travel farther before air resistance stops them. The stretch factor simulates the elongation that happens when a droplet slides along a surface after impact—faster droplets stretch more.
2. Drip Physics
Gravity is the silent sculptor of paint splatter art. When Pollock worked on vertical surfaces or tilted his canvases, paint would drip downward in long, uneven trails. This example simulates drip physics: move your mouse to position the paint source, and drops fall under gravity. Viscosity controls speed—thick drops crawl, thin ones race. When drops reach the bottom edge, they splash into smaller fragments.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Drip Physics</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { background: #1a1a2e; overflow: hidden; cursor: none; }
canvas { display: block; }
.info {
position: fixed; bottom: 16px; left: 50%; transform: translateX(-50%);
font-family: sans-serif; font-size: 14px; color: #aaa;
background: rgba(0,0,0,0.6); padding: 6px 14px; border-radius: 20px;
}
</style>
</head>
<body>
<canvas id="c"></canvas>
<div class="info">Move mouse to drip paint</div>
<script>
var canvas = document.getElementById('c');
var ctx = canvas.getContext('2d');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
var colors = ['#e63946', '#f1faee', '#a8dadc', '#457b9d', '#e9c46a', '#f4a261'];
var drops = [];
var mouseX = canvas.width / 2;
var mouseY = 60;
var frame = 0;
function Drop(x, y, color) {
this.x = x;
this.y = y;
this.color = color;
this.radius = 3 + Math.random() * 5;
this.viscosity = 0.3 + Math.random() * 0.7;
this.vy = 0;
this.vx = (Math.random() - 0.5) * 0.5;
this.trail = [];
this.alive = true;
}
canvas.addEventListener('mousemove', function(e) {
mouseX = e.clientX;
mouseY = e.clientY;
});
function splash(x, y, color) {
for (var i = 0; i < 6; i++) {
var angle = -Math.PI * Math.random();
var d = new Drop(x + Math.cos(angle) * 10, y, color);
d.vx = Math.cos(angle) * (1 + Math.random() * 2);
d.vy = -(1 + Math.random() * 3);
d.radius = 1 + Math.random() * 2;
drops.push(d);
}
}
function update() {
frame++;
if (frame % 4 === 0) {
var c = colors[Math.floor(Math.random() * colors.length)];
drops.push(new Drop(mouseX + (Math.random() - 0.5) * 20, mouseY, c));
}
for (var i = drops.length - 1; i >= 0; i--) {
var d = drops[i];
if (!d.alive) continue;
d.vy += 0.15 / d.viscosity;
d.x += d.vx;
d.y += d.vy;
d.trail.push({ x: d.x, y: d.y, r: d.radius * (0.5 + d.viscosity * 0.5) });
if (d.trail.length > 200) d.trail.shift();
if (d.y > canvas.height - 10) {
d.alive = false;
splash(d.x, canvas.height - 10, d.color);
}
// merge nearby drops
for (var j = i - 1; j >= Math.max(0, i - 10); j--) {
var o = drops[j];
if (!o.alive) continue;
var dist = Math.sqrt((d.x - o.x) * (d.x - o.x) + (d.y - o.y) * (d.y - o.y));
if (dist < d.radius + o.radius) {
d.radius = Math.sqrt(d.radius * d.radius + o.radius * o.radius);
d.vy = (d.vy + o.vy) / 2;
o.alive = false;
}
}
}
// remove dead drops older than needed
if (drops.length > 500) {
drops = drops.filter(function(d) { return d.alive || d.trail.length > 0; });
}
}
function draw() {
// semi-transparent overlay for slight fade
ctx.fillStyle = 'rgba(26,26,46,0.02)';
ctx.fillRect(0, 0, canvas.width, canvas.height);
for (var i = 0; i < drops.length; i++) {
var d = drops[i];
// draw trail
for (var t = 0; t < d.trail.length; t++) {
var p = d.trail[t];
ctx.beginPath();
ctx.arc(p.x, p.y, p.r * 0.7, 0, Math.PI * 2);
ctx.fillStyle = d.color;
ctx.globalAlpha = 0.3;
ctx.fill();
}
if (d.alive) {
ctx.beginPath();
ctx.arc(d.x, d.y, d.radius, 0, Math.PI * 2);
ctx.fillStyle = d.color;
ctx.globalAlpha = 0.9;
ctx.fill();
}
}
ctx.globalAlpha = 1;
// cursor indicator
ctx.beginPath();
ctx.arc(mouseX, mouseY, 6, 0, Math.PI * 2);
ctx.strokeStyle = 'rgba(255,255,255,0.5)';
ctx.lineWidth = 1;
ctx.stroke();
}
function loop() {
update();
draw();
requestAnimationFrame(loop);
}
loop();
window.addEventListener('resize', function() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
});
</script>
</body>
</html>
Notice how the viscosity property inversely affects gravity’s pull: d.vy += 0.15 / d.viscosity. A highly viscous drop (value near 1.0) accelerates slowly, mimicking thick acrylic or enamel paint. The merge check simulates surface tension—when two drops overlap they combine into one larger drop, conserving area rather than radius so the pooling looks natural.
3. Action Painting Simulator
Action painting is about the gesture. Pollock described his process: “When I am in my painting, I’m not aware of what I’m doing.” This example captures that energy by mapping mouse velocity to paint trajectory. Move slowly and paint drips straight down; fling the mouse across the screen and paint scatters in wide, chaotic arcs. Multiple colors layer with additive opacity, building depth and complexity over time.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Action Painting Simulator</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { background: #f5f0e8; overflow: hidden; cursor: crosshair; }
canvas { display: block; }
.info {
position: fixed; bottom: 16px; left: 50%; transform: translateX(-50%);
font-family: sans-serif; font-size: 14px; color: #666;
background: rgba(255,255,255,0.8); padding: 6px 14px; border-radius: 20px;
}
</style>
</head>
<body>
<canvas id="c"></canvas>
<div class="info">Move mouse to fling paint — faster = more scatter</div>
<script>
var canvas = document.getElementById('c');
var ctx = canvas.getContext('2d');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
ctx.fillStyle = '#f5f0e8';
ctx.fillRect(0, 0, canvas.width, canvas.height);
var palette = [
'#1a1a1a', '#e63946', '#457b9d', '#e9c46a',
'#2a9d8f', '#f4a261', '#264653'
];
var currentColor = palette[0];
var prevX = 0, prevY = 0;
var isDown = false;
var colorTimer = 0;
canvas.addEventListener('mousedown', function(e) {
isDown = true;
prevX = e.clientX;
prevY = e.clientY;
currentColor = palette[Math.floor(Math.random() * palette.length)];
});
canvas.addEventListener('mouseup', function() { isDown = false; });
canvas.addEventListener('mousemove', function(e) {
if (!isDown) return;
var x = e.clientX;
var y = e.clientY;
var dx = x - prevX;
var dy = y - prevY;
var speed = Math.sqrt(dx * dx + dy * dy);
var angle = Math.atan2(dy, dx);
colorTimer++;
if (colorTimer % 60 === 0) {
currentColor = palette[Math.floor(Math.random() * palette.length)];
}
// main stroke
var steps = Math.max(1, Math.floor(speed / 3));
for (var s = 0; s < steps; s++) {
var t = s / steps;
var sx = prevX + dx * t;
var sy = prevY + dy * t;
ctx.beginPath();
ctx.arc(sx, sy, 1.5 + Math.random() * 2, 0, Math.PI * 2);
ctx.fillStyle = currentColor;
ctx.globalAlpha = 0.5 + Math.random() * 0.3;
ctx.fill();
}
// splatter based on speed
if (speed > 8) {
var count = Math.floor(speed / 3);
for (var i = 0; i < count; i++) {
var splatAngle = angle + (Math.random() - 0.5) * Math.PI * 0.8;
var dist = speed * (0.5 + Math.random() * 2);
var sx2 = x + Math.cos(splatAngle) * dist;
var sy2 = y + Math.sin(splatAngle) * dist;
var r = 1 + Math.random() * 3;
ctx.beginPath();
ctx.arc(sx2, sy2, r, 0, Math.PI * 2);
ctx.fillStyle = currentColor;
ctx.globalAlpha = 0.3 + Math.random() * 0.4;
ctx.fill();
}
}
// drip trails for slow movement
if (speed < 5 && Math.random() < 0.3) {
var dripLen = 10 + Math.random() * 40;
for (var d = 0; d < dripLen; d++) {
ctx.beginPath();
ctx.arc(x + (Math.random() - 0.5) * 3, y + d, 0.5 + Math.random(), 0, Math.PI * 2);
ctx.fillStyle = currentColor;
ctx.globalAlpha = 0.2 * (1 - d / dripLen);
ctx.fill();
}
}
ctx.globalAlpha = 1;
prevX = x;
prevY = y;
});
window.addEventListener('resize', function() {
var img = ctx.getImageData(0, 0, canvas.width, canvas.height);
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
ctx.putImageData(img, 0, 0);
});
</script>
</body>
</html>
The relationship between mouse speed and scatter radius is the heart of action painting simulation. At low speeds (below 5 pixels per frame), paint drips downward under gravity. At high speeds (above 8), droplets scatter in a cone centered on the movement direction. This dual behavior mirrors how a real brush or stick behaves when moved through wet paint—slow motions leave pooling drips, while fast flicks send paint flying.
4. Watercolor Bloom Effect
Watercolor behaves differently from oil or enamel paint. When a wet drop hits wet paper, it spreads organically with feathered edges—a phenomenon watercolorists call “blooming” or “backruns.” To simulate this, we grow a circle over time with a noise-displaced boundary. The outer edge fades in opacity, creating the characteristic soft halo of watercolor on damp paper.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Watercolor Bloom Effect</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { background: #faf8f0; overflow: hidden; cursor: crosshair; }
canvas { display: block; }
.info {
position: fixed; bottom: 16px; left: 50%; transform: translateX(-50%);
font-family: sans-serif; font-size: 14px; color: #888;
background: rgba(255,255,255,0.8); padding: 6px 14px; border-radius: 20px;
}
</style>
</head>
<body>
<canvas id="c"></canvas>
<div class="info">Click to place watercolor drops</div>
<script>
var canvas = document.getElementById('c');
var ctx = canvas.getContext('2d');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
ctx.fillStyle = '#faf8f0';
ctx.fillRect(0, 0, canvas.width, canvas.height);
var blooms = [];
var watercolors = [
{ r: 41, g: 98, b: 155 },
{ r: 183, g: 58, b: 67 },
{ r: 42, g: 157, b: 143 },
{ r: 233, g: 196, b: 106 },
{ r: 100, g: 50, b: 120 },
{ r: 218, g: 119, b: 72 }
];
// Simple noise function
function noise(x, y) {
var n = Math.sin(x * 12.9898 + y * 78.233) * 43758.5453;
return n - Math.floor(n);
}
function smoothNoise(x, y) {
var ix = Math.floor(x);
var iy = Math.floor(y);
var fx = x - ix;
var fy = y - iy;
fx = fx * fx * (3 - 2 * fx);
fy = fy * fy * (3 - 2 * fy);
var a = noise(ix, iy);
var b = noise(ix + 1, iy);
var c = noise(ix, iy + 1);
var d = noise(ix + 1, iy + 1);
return a + (b - a) * fx + (c - a) * fy + (a - b - c + d) * fx * fy;
}
function Bloom(x, y) {
var col = watercolors[Math.floor(Math.random() * watercolors.length)];
this.x = x;
this.y = y;
this.color = col;
this.maxRadius = 40 + Math.random() * 80;
this.radius = 5;
this.growth = 0.3 + Math.random() * 0.5;
this.seed = Math.random() * 100;
this.done = false;
this.layers = 5 + Math.floor(Math.random() * 4);
}
function drawBloom(b) {
if (b.done) return;
b.radius += b.growth;
if (b.radius >= b.maxRadius) {
b.radius = b.maxRadius;
b.done = true;
}
for (var layer = 0; layer < b.layers; layer++) {
var layerR = b.radius * (0.3 + 0.7 * layer / b.layers);
var alpha = 0.02 * (1 - layer / b.layers);
var segments = 64;
ctx.beginPath();
for (var i = 0; i <= segments; i++) {
var angle = (i / segments) * Math.PI * 2;
var n = smoothNoise(
Math.cos(angle) * 2 + b.seed,
Math.sin(angle) * 2 + b.seed + layer * 0.5
);
var r = layerR * (0.8 + n * 0.4);
var px = b.x + Math.cos(angle) * r;
var py = b.y + Math.sin(angle) * r;
if (i === 0) ctx.moveTo(px, py);
else ctx.lineTo(px, py);
}
ctx.closePath();
ctx.fillStyle = 'rgba(' + b.color.r + ',' + b.color.g + ',' + b.color.b + ',' + alpha + ')';
ctx.fill();
}
}
canvas.addEventListener('click', function(e) {
blooms.push(new Bloom(e.clientX, e.clientY));
});
function loop() {
for (var i = 0; i < blooms.length; i++) {
drawBloom(blooms[i]);
}
requestAnimationFrame(loop);
}
loop();
window.addEventListener('resize', function() {
var img = ctx.getImageData(0, 0, canvas.width, canvas.height);
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
ctx.putImageData(img, 0, 0);
});
</script>
</body>
</html>
The organic edge comes from the smoothNoise function displacing each point on the boundary. By using different noise seeds for each layer and bloom, every drop has a unique irregular shape. The multiple semi-transparent layers build up color density in the center while fading at the edges—precisely how watercolor pigment behaves as water evaporates from the outside in.
5. Ink Splash Simulation
Ink splashes have a distinctive character: a dense central impact surrounded by radiating tendrils that branch and taper like river deltas seen from space. This simulation uses Bezier curves to create those organic tendril shapes. Micro-droplets scatter along each tendril path, adding the fine satellite spray you see in real ink splashes. A dark blue-black ink on a cream background evokes traditional calligraphy and sumi-e painting.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Ink Splash Simulation</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { background: #faf5e6; overflow: hidden; cursor: crosshair; }
canvas { display: block; }
.info {
position: fixed; bottom: 16px; left: 50%; transform: translateX(-50%);
font-family: sans-serif; font-size: 14px; color: #888;
background: rgba(255,255,255,0.8); padding: 6px 14px; border-radius: 20px;
}
</style>
</head>
<body>
<canvas id="c"></canvas>
<div class="info">Click to create ink splashes</div>
<script>
var canvas = document.getElementById('c');
var ctx = canvas.getContext('2d');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
ctx.fillStyle = '#faf5e6';
ctx.fillRect(0, 0, canvas.width, canvas.height);
function inkSplash(cx, cy) {
var inkR = Math.floor(15 + Math.random() * 15);
var inkG = Math.floor(15 + Math.random() * 15);
var inkB = Math.floor(35 + Math.random() * 25);
// Central blob
var blobRadius = 15 + Math.random() * 25;
for (var l = 0; l < 10; l++) {
ctx.beginPath();
var pts = 32;
for (var i = 0; i <= pts; i++) {
var a = (i / pts) * Math.PI * 2;
var r = blobRadius * (0.7 + Math.random() * 0.5);
var px = cx + Math.cos(a) * r;
var py = cy + Math.sin(a) * r;
if (i === 0) ctx.moveTo(px, py);
else ctx.lineTo(px, py);
}
ctx.closePath();
ctx.fillStyle = 'rgba(' + inkR + ',' + inkG + ',' + inkB + ',0.15)';
ctx.fill();
}
// Tendrils
var tendrilCount = 6 + Math.floor(Math.random() * 8);
for (var t = 0; t < tendrilCount; t++) {
var angle = (t / tendrilCount) * Math.PI * 2 + (Math.random() - 0.5) * 0.5;
var length = 60 + Math.random() * 120;
var cp1Dist = length * (0.3 + Math.random() * 0.3);
var cp2Dist = length * (0.6 + Math.random() * 0.3);
var drift = (Math.random() - 0.5) * 1.2;
var cp1x = cx + Math.cos(angle + drift * 0.3) * cp1Dist;
var cp1y = cy + Math.sin(angle + drift * 0.3) * cp1Dist;
var cp2x = cx + Math.cos(angle + drift * 0.7) * cp2Dist;
var cp2y = cy + Math.sin(angle + drift * 0.7) * cp2Dist;
var ex = cx + Math.cos(angle + drift) * length;
var ey = cy + Math.sin(angle + drift) * length;
// Draw tapered tendril
var segments = 20;
for (var s = 0; s < segments; s++) {
var t1 = s / segments;
var t2 = (s + 1) / segments;
var p1 = bezierPoint(cx, cy, cp1x, cp1y, cp2x, cp2y, ex, ey, t1);
var p2 = bezierPoint(cx, cy, cp1x, cp1y, cp2x, cp2y, ex, ey, t2);
var width = (1 - t1) * (4 + Math.random() * 3);
ctx.beginPath();
ctx.moveTo(p1.x, p1.y);
ctx.lineTo(p2.x, p2.y);
ctx.lineWidth = width;
ctx.strokeStyle = 'rgba(' + inkR + ',' + inkG + ',' + inkB + ',' + (0.6 * (1 - t1)) + ')';
ctx.lineCap = 'round';
ctx.stroke();
// Micro-droplets along tendril
if (Math.random() < 0.3) {
var offAngle = angle + (Math.random() - 0.5) * Math.PI;
var offDist = 3 + Math.random() * 10;
ctx.beginPath();
ctx.arc(
p1.x + Math.cos(offAngle) * offDist,
p1.y + Math.sin(offAngle) * offDist,
0.5 + Math.random() * 1.5, 0, Math.PI * 2
);
ctx.fillStyle = 'rgba(' + inkR + ',' + inkG + ',' + inkB + ',0.5)';
ctx.fill();
}
}
}
// Satellite droplets
for (var s = 0; s < 30; s++) {
var sa = Math.random() * Math.PI * 2;
var sd = blobRadius + 20 + Math.random() * 100;
ctx.beginPath();
ctx.arc(cx + Math.cos(sa) * sd, cy + Math.sin(sa) * sd, 0.5 + Math.random() * 2, 0, Math.PI * 2);
ctx.fillStyle = 'rgba(' + inkR + ',' + inkG + ',' + inkB + ',0.4)';
ctx.fill();
}
}
function bezierPoint(x0, y0, cp1x, cp1y, cp2x, cp2y, x1, y1, t) {
var mt = 1 - t;
var mt2 = mt * mt;
var mt3 = mt2 * mt;
var t2 = t * t;
var t3 = t2 * t;
return {
x: mt3 * x0 + 3 * mt2 * t * cp1x + 3 * mt * t2 * cp2x + t3 * x1,
y: mt3 * y0 + 3 * mt2 * t * cp1y + 3 * mt * t2 * cp2y + t3 * y1
};
}
canvas.addEventListener('click', function(e) {
inkSplash(e.clientX, e.clientY);
});
window.addEventListener('resize', function() {
var img = ctx.getImageData(0, 0, canvas.width, canvas.height);
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
ctx.putImageData(img, 0, 0);
});
</script>
</body>
</html>
Cubic Bezier curves are the secret weapon for organic tendril shapes. Each tendril has two control points that drift away from the straight-line path, creating natural S-curves and hooks. The drift variable adds angular deviation so tendrils don’t all point rigidly outward. Combined with the tapering stroke width and scattered micro-droplets, the result is remarkably close to a real ink splash photographed at high resolution.
6. Spray Paint Effect
Aerosol spray paint produces a distinctively different splatter pattern: a dense core that fades radially outward following a Gaussian distribution. Each “particle” is a tiny dot placed with slight randomness. Holding the mouse still builds up paint density, simulating what happens when you hold a spray can too close to the wall. Press keys 1 through 5 to switch between five spray colors.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Spray Paint Effect</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { background: #333; overflow: hidden; cursor: none; }
canvas { display: block; }
.info {
position: fixed; bottom: 16px; left: 50%; transform: translateX(-50%);
font-family: sans-serif; font-size: 14px; color: #ccc;
background: rgba(0,0,0,0.6); padding: 6px 14px; border-radius: 20px;
}
.palette {
position: fixed; top: 16px; left: 50%; transform: translateX(-50%);
display: flex; gap: 8px;
}
.palette .swatch {
width: 28px; height: 28px; border-radius: 50%; border: 2px solid rgba(255,255,255,0.4);
font-family: sans-serif; font-size: 11px; color: #fff;
display: flex; align-items: center; justify-content: center;
}
.palette .swatch.active { border-color: #fff; box-shadow: 0 0 8px rgba(255,255,255,0.6); }
</style>
</head>
<body>
<canvas id="c"></canvas>
<div class="palette" id="pal"></div>
<div class="info">Hold mouse to spray — keys 1-5 change color</div>
<script>
var canvas = document.getElementById('c');
var ctx = canvas.getContext('2d');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
ctx.fillStyle = '#333';
ctx.fillRect(0, 0, canvas.width, canvas.height);
var colors = [
{ r: 230, g: 50, b: 50 },
{ r: 50, g: 120, b: 230 },
{ r: 255, g: 220, b: 50 },
{ r: 50, g: 200, b: 100 },
{ r: 240, g: 240, b: 240 }
];
var currentIdx = 0;
var mouseX = 0, mouseY = 0;
var spraying = false;
// Build palette UI
var palEl = document.getElementById('pal');
for (var i = 0; i < colors.length; i++) {
var s = document.createElement('div');
s.className = 'swatch' + (i === 0 ? ' active' : '');
s.style.background = 'rgb(' + colors[i].r + ',' + colors[i].g + ',' + colors[i].b + ')';
s.textContent = String(i + 1);
palEl.appendChild(s);
}
function setColor(idx) {
if (idx < 0 || idx >= colors.length) return;
currentIdx = idx;
var swatches = document.querySelectorAll('.swatch');
for (var i = 0; i < swatches.length; i++) {
swatches[i].className = 'swatch' + (i === idx ? ' active' : '');
}
}
// Gaussian random via Box-Muller
function gaussRandom() {
var u = 1 - Math.random();
var v = Math.random();
return Math.sqrt(-2 * Math.log(u)) * Math.cos(2 * Math.PI * v);
}
function spray(x, y) {
var col = colors[currentIdx];
var count = 30;
var radius = 35;
for (var i = 0; i < count; i++) {
var gx = gaussRandom() * radius * 0.4;
var gy = gaussRandom() * radius * 0.4;
var dist = Math.sqrt(gx * gx + gy * gy);
var alpha = Math.max(0, 0.15 * (1 - dist / (radius * 1.2)));
var size = 1 + Math.random() * 2;
ctx.beginPath();
ctx.arc(x + gx, y + gy, size, 0, Math.PI * 2);
ctx.fillStyle = 'rgba(' + col.r + ',' + col.g + ',' + col.b + ',' + alpha + ')';
ctx.fill();
}
}
canvas.addEventListener('mousedown', function(e) {
spraying = true;
mouseX = e.clientX;
mouseY = e.clientY;
});
canvas.addEventListener('mouseup', function() { spraying = false; });
canvas.addEventListener('mousemove', function(e) {
mouseX = e.clientX;
mouseY = e.clientY;
});
document.addEventListener('keydown', function(e) {
var n = parseInt(e.key);
if (n >= 1 && n <= 5) setColor(n - 1);
});
function loop() {
if (spraying) spray(mouseX, mouseY);
// Cursor
ctx.save();
ctx.globalCompositeOperation = 'source-over';
ctx.beginPath();
ctx.arc(mouseX, mouseY, 18, 0, Math.PI * 2);
ctx.strokeStyle = 'rgba(255,255,255,0.3)';
ctx.lineWidth = 1;
ctx.stroke();
ctx.beginPath();
ctx.arc(mouseX, mouseY, 2, 0, Math.PI * 2);
ctx.fillStyle = 'rgba(255,255,255,0.5)';
ctx.fill();
ctx.restore();
requestAnimationFrame(loop);
}
loop();
window.addEventListener('resize', function() {
var img = ctx.getImageData(0, 0, canvas.width, canvas.height);
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
ctx.putImageData(img, 0, 0);
});
</script>
</body>
</html>
The Gaussian distribution via the Box-Muller transform is crucial for realistic spray simulation. Real aerosol nozzles produce a cone of droplets whose density follows a bell curve—thick in the center, rapidly thinning at the edges. The alpha value also decreases with distance from center, so the visual density falloff matches the particle density falloff. When you hold still, particles accumulate and the color saturates, just like over-spraying a real wall.
From Canvas to Code: A Brief History of Splatter
Jackson Pollock did not invent paint splatter—house painters and children had been doing it for centuries—but he transformed it into high art. Working in his Springs, Long Island studio from 1947 onward, Pollock developed his “drip technique”: laying large canvases flat on the floor and pouring, dripping, and flinging commercial house paint with sticks, trowels, and hardened brushes. The resulting paintings—Number 1A, 1948, Autumn Rhythm, One: Number 31, 1950—shattered conventions about what a painting could be.
Art critic Harold Rosenberg coined the term “action painting” in 1952, arguing that the canvas had become “an arena in which to act” rather than a space to represent. The gesture mattered as much as the result. Decades later, physicist Richard Taylor discovered that Pollock’s drip paintings contain fractal patterns—self-similar structures at multiple scales, with fractal dimensions that increased over his career from roughly 1.45 to 1.72. This finding bridged art and mathematics, suggesting that Pollock’s intuitive gestures produced structures mathematically akin to natural phenomena like coastlines and turbulence.
Computational artists picked up where Pollock left off. In the 1960s, pioneers like Vera Molnár and Georg Nees used early plotters to explore randomness and order. By the 2000s, Processing and later p5.js made generative art accessible to anyone with a browser. Today, paint splatter simulation is a popular exercise in creative coding—it combines physics simulation (gravity, drag, surface tension), randomness, and aesthetic judgment into a single satisfying challenge.
7. Pollock Drip Painting
This fully autonomous simulation models Pollock’s technique: an invisible “arm” swings back and forth above the canvas like a pendulum, dripping paint in continuous streams. The arm oscillates with a compound frequency (two sine waves summed), producing complex non-repeating trajectories. Occasional “flick” gestures scatter droplets in wide arcs. The simulation runs on its own, building up layers of four or five paint colors on a cream canvas.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Pollock Drip Painting</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { background: #2a2a2a; overflow: hidden; display: flex; align-items: center; justify-content: center; height: 100vh; }
canvas { box-shadow: 0 4px 30px rgba(0,0,0,0.5); }
.info {
position: fixed; bottom: 16px; left: 50%; transform: translateX(-50%);
font-family: sans-serif; font-size: 14px; color: #aaa;
background: rgba(0,0,0,0.6); padding: 6px 14px; border-radius: 20px;
}
</style>
</head>
<body>
<canvas id="c"></canvas>
<div class="info">Autonomous Pollock drip painting — watch it evolve</div>
<script>
var canvas = document.getElementById('c');
var ctx = canvas.getContext('2d');
var W = Math.min(window.innerWidth - 40, 1200);
var H = Math.min(window.innerHeight - 80, 800);
canvas.width = W;
canvas.height = H;
// Cream canvas
ctx.fillStyle = '#f5f0e0';
ctx.fillRect(0, 0, W, H);
var colors = [
'rgba(20,20,20,', // black
'rgba(180,40,30,', // deep red
'rgba(60,80,140,', // dark blue
'rgba(200,180,60,', // ochre
'rgba(240,235,220,' // white drizzle
];
var time = 0;
var armX = W / 2;
var armY = H * 0.3;
var currentColor = 0;
var colorChangeTime = 0;
var dripPoints = [];
function pickColor() {
currentColor = Math.floor(Math.random() * colors.length);
colorChangeTime = time + 200 + Math.random() * 400;
}
pickColor();
function update() {
time++;
if (time > colorChangeTime) pickColor();
// Pendulum arm motion (compound oscillation)
var freq1 = 0.013 + Math.sin(time * 0.0003) * 0.005;
var freq2 = 0.031;
armX = W * 0.15 + (W * 0.7) * (0.5 + 0.3 * Math.sin(time * freq1) + 0.2 * Math.sin(time * freq2));
armY = H * 0.2 + H * 0.15 * Math.sin(time * 0.007) + H * 0.1 * Math.cos(time * 0.019);
// Drip continuously
if (Math.random() < 0.6) {
dripPoints.push({
x: armX + (Math.random() - 0.5) * 15,
y: armY,
vx: (Math.random() - 0.5) * 1.5,
vy: 1 + Math.random() * 2,
color: currentColor,
size: 1 + Math.random() * 2.5,
life: 0
});
}
// Occasional flick
if (Math.random() < 0.008) {
var flickAngle = Math.random() * Math.PI * 2;
var flickCount = 10 + Math.floor(Math.random() * 20);
for (var f = 0; f < flickCount; f++) {
var fa = flickAngle + (Math.random() - 0.5) * 1.2;
var fSpeed = 3 + Math.random() * 8;
dripPoints.push({
x: armX,
y: armY,
vx: Math.cos(fa) * fSpeed,
vy: Math.sin(fa) * fSpeed,
color: currentColor,
size: 0.5 + Math.random() * 2,
life: 0
});
}
}
// Simulate drips
for (var i = dripPoints.length - 1; i >= 0; i--) {
var d = dripPoints[i];
d.vy += 0.12; // gravity
d.x += d.vx;
d.y += d.vy;
d.vx *= 0.99; // air drag
d.life++;
// Draw on canvas
if (d.x >= 0 && d.x <= W && d.y >= 0 && d.y <= H) {
ctx.beginPath();
ctx.arc(d.x, d.y, d.size, 0, Math.PI * 2);
ctx.fillStyle = colors[d.color] + (0.3 + Math.random() * 0.4) + ')';
ctx.fill();
}
// Remove if out of bounds or too old
if (d.y > H + 10 || d.x < -10 || d.x > W + 10 || d.life > 300) {
dripPoints.splice(i, 1);
}
}
}
function loop() {
for (var step = 0; step < 3; step++) {
update();
}
requestAnimationFrame(loop);
}
loop();
</script>
</body>
</html>
The compound oscillation—two sine waves with incommensurate frequencies—is key to producing Pollock-like trajectories. If the frequencies have a simple ratio (like 2:1), the arm traces a predictable Lissajous figure. By using irrational-ish ratios and slowly modulating one frequency over time with Math.sin(time * 0.0003), the arm’s path never exactly repeats, filling the canvas more evenly over time. This mirrors how a real painter shifts their stance and reach throughout a session.
8. Generative Splatter Composition
The final example combines everything we have built: background washes, large splatters, fine mist layers, drip trails, and ink tendrils. Each technique is applied at a different scale and opacity, building up a complete abstract composition automatically on load. Click anywhere to regenerate with a fresh random seed and new color harmony.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Generative Splatter Composition</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { background: #1a1a1a; overflow: hidden; cursor: pointer;
display: flex; align-items: center; justify-content: center; height: 100vh; }
canvas { box-shadow: 0 4px 30px rgba(0,0,0,0.5); }
.info {
position: fixed; bottom: 16px; left: 50%; transform: translateX(-50%);
font-family: sans-serif; font-size: 14px; color: #aaa;
background: rgba(0,0,0,0.6); padding: 6px 14px; border-radius: 20px;
}
</style>
</head>
<body>
<canvas id="c"></canvas>
<div class="info">Click to regenerate composition</div>
<script>
var canvas = document.getElementById('c');
var ctx = canvas.getContext('2d');
var W = Math.min(window.innerWidth - 40, 1200);
var H = Math.min(window.innerHeight - 80, 900);
canvas.width = W;
canvas.height = H;
function rand(min, max) { return min + Math.random() * (max - min); }
function gaussRand() {
var u = 1 - Math.random();
var v = Math.random();
return Math.sqrt(-2 * Math.log(u)) * Math.cos(2 * Math.PI * v);
}
function noise(x, y) {
var n = Math.sin(x * 12.9898 + y * 78.233) * 43758.5453;
return n - Math.floor(n);
}
function generatePalette() {
var hueBase = Math.random() * 360;
var pal = [];
var offsets = [0, 30, 160, 200, 280];
for (var i = 0; i < offsets.length; i++) {
var h = (hueBase + offsets[i]) % 360;
var s = 50 + Math.random() * 40;
var l = 35 + Math.random() * 30;
pal.push(hslToRgb(h, s, l));
}
return pal;
}
function hslToRgb(h, s, l) {
s /= 100; l /= 100;
var c = (1 - Math.abs(2 * l - 1)) * s;
var x = c * (1 - Math.abs((h / 60) % 2 - 1));
var m = l - c / 2;
var r, g, b;
if (h < 60) { r = c; g = x; b = 0; }
else if (h < 120) { r = x; g = c; b = 0; }
else if (h < 180) { r = 0; g = c; b = x; }
else if (h < 240) { r = 0; g = x; b = c; }
else if (h < 300) { r = x; g = 0; b = c; }
else { r = c; g = 0; b = x; }
return {
r: Math.round((r + m) * 255),
g: Math.round((g + m) * 255),
b: Math.round((b + m) * 255)
};
}
function rgba(col, a) {
return 'rgba(' + col.r + ',' + col.g + ',' + col.b + ',' + a + ')';
}
// -- Layer 1: Background wash --
function drawWash(pal) {
for (var i = 0; i < 3; i++) {
var col = pal[Math.floor(Math.random() * pal.length)];
var grd = ctx.createRadialGradient(
rand(W * 0.2, W * 0.8), rand(H * 0.2, H * 0.8), 0,
rand(W * 0.2, W * 0.8), rand(H * 0.2, H * 0.8), rand(200, 500)
);
grd.addColorStop(0, rgba(col, 0.08));
grd.addColorStop(1, rgba(col, 0));
ctx.fillStyle = grd;
ctx.fillRect(0, 0, W, H);
}
}
// -- Layer 2: Large splatters --
function drawSplatters(pal) {
var count = 4 + Math.floor(Math.random() * 5);
for (var s = 0; s < count; s++) {
var cx = rand(W * 0.1, W * 0.9);
var cy = rand(H * 0.1, H * 0.9);
var col = pal[Math.floor(Math.random() * pal.length)];
var dropCount = 40 + Math.floor(Math.random() * 30);
// central blob
ctx.beginPath();
ctx.arc(cx, cy, rand(10, 30), 0, Math.PI * 2);
ctx.fillStyle = rgba(col, rand(0.3, 0.6));
ctx.fill();
for (var d = 0; d < dropCount; d++) {
var a = Math.random() * Math.PI * 2;
var dist = Math.random() * Math.random() * 150;
var r = 1 + Math.random() * 5 * (1 - dist / 150);
ctx.beginPath();
ctx.arc(cx + Math.cos(a) * dist, cy + Math.sin(a) * dist, r, 0, Math.PI * 2);
ctx.fillStyle = rgba(col, rand(0.2, 0.5));
ctx.fill();
}
}
}
// -- Layer 3: Fine mist --
function drawMist(pal) {
var col = pal[Math.floor(Math.random() * pal.length)];
for (var i = 0; i < 2000; i++) {
var x = rand(0, W);
var y = rand(0, H);
var density = noise(x * 0.01, y * 0.01);
if (Math.random() > density) continue;
ctx.beginPath();
ctx.arc(x, y, 0.5 + Math.random() * 1.5, 0, Math.PI * 2);
ctx.fillStyle = rgba(col, rand(0.05, 0.15));
ctx.fill();
}
}
// -- Layer 4: Drip trails --
function drawDrips(pal) {
var dripCount = 6 + Math.floor(Math.random() * 8);
for (var d = 0; d < dripCount; d++) {
var col = pal[Math.floor(Math.random() * pal.length)];
var x = rand(W * 0.05, W * 0.95);
var y = rand(H * 0.05, H * 0.3);
var vy = 0;
var thickness = 1 + Math.random() * 3;
var steps = 50 + Math.floor(Math.random() * 150);
for (var s = 0; s < steps; s++) {
vy += 0.3 + Math.random() * 0.2;
x += (Math.random() - 0.5) * 1.5;
y += vy * 0.3;
if (y > H) break;
var t = thickness * (1 - s / steps * 0.5);
ctx.beginPath();
ctx.arc(x, y, t, 0, Math.PI * 2);
ctx.fillStyle = rgba(col, rand(0.15, 0.35));
ctx.fill();
}
}
}
// -- Layer 5: Ink tendrils --
function drawTendrils(pal) {
var count = 2 + Math.floor(Math.random() * 3);
for (var t = 0; t < count; t++) {
var col = pal[Math.floor(Math.random() * pal.length)];
var cx = rand(W * 0.2, W * 0.8);
var cy = rand(H * 0.2, H * 0.8);
var arms = 5 + Math.floor(Math.random() * 6);
for (var a = 0; a < arms; a++) {
var angle = (a / arms) * Math.PI * 2 + rand(-0.3, 0.3);
var len = 50 + Math.random() * 120;
var segs = 15;
var px = cx, py = cy;
for (var s = 0; s < segs; s++) {
var frac = s / segs;
var nx = px + Math.cos(angle + (Math.random() - 0.5) * 0.4) * (len / segs);
var ny = py + Math.sin(angle + (Math.random() - 0.5) * 0.4) * (len / segs);
ctx.beginPath();
ctx.moveTo(px, py);
ctx.lineTo(nx, ny);
ctx.lineWidth = (1 - frac) * 3 + 0.5;
ctx.strokeStyle = rgba(col, 0.4 * (1 - frac));
ctx.lineCap = 'round';
ctx.stroke();
px = nx;
py = ny;
}
}
}
}
function compose() {
// Cream base
ctx.fillStyle = '#f5f0e0';
ctx.fillRect(0, 0, W, H);
var pal = generatePalette();
drawWash(pal);
drawSplatters(pal);
drawMist(pal);
drawDrips(pal);
drawTendrils(pal);
drawSplatters(pal); // second pass, more splatters on top
drawMist(pal);
}
compose();
canvas.addEventListener('click', function() {
compose();
});
window.addEventListener('resize', function() {
W = Math.min(window.innerWidth - 40, 1200);
H = Math.min(window.innerHeight - 80, 900);
canvas.width = W;
canvas.height = H;
compose();
});
</script>
</body>
</html>
The layering order matters enormously. Background washes go down first as a tinted ground. Large splatters establish the composition’s focal points. Fine mist adds atmospheric texture. Drip trails introduce vertical rhythm. Ink tendrils provide linear energy. The second pass of splatters and mist on top ties the layers together with overlapping forms. This is exactly how a painter builds an abstract composition—working in stages from broad fields of color to fine detail.
Performance Tips for Paint Splatter Simulations
Paint splatter art can push a browser canvas hard, especially when thousands of droplets accumulate. Here are practical tips to keep your simulations running smoothly:
- Draw directly, don’t store everything. In most splatter simulations, once paint hits the canvas it never moves again. Draw it immediately and discard the object. Only keep active (moving) particles in memory.
- Batch your draw calls. Grouping particles by color and opacity lets you set
fillStyleonce and draw many circles before changing state. This reduces the number of GPU state switches the browser must perform. - Use
globalCompositeOperationwisely. Operations like'multiply'and'screen'create beautiful blending effects but are more expensive than the default'source-over'. Use them sparingly or on a separate offscreen canvas that you composite once per frame. - Limit particle counts. Cap your active particle array at a reasonable size (500–2000 depending on the effect). When the cap is reached, remove the oldest particles first.
- Use
requestAnimationFrame, notsetInterval. It syncs with the display refresh rate and automatically pauses when the tab is hidden, saving CPU and battery. - Consider offscreen canvases for layers. When compositing multiple effects (as in example 8), render each layer to its own offscreen canvas, then draw them all onto the visible canvas. This avoids re-rendering static layers every frame.
- Profile with DevTools. Chrome’s Performance panel and the Canvas profiler reveal exactly which draw calls are expensive. A single
getImageDatacall on a large canvas can take milliseconds—avoid it in animation loops.
Taking Your Splatter Art Further
The eight examples in this guide cover the fundamental physics and aesthetics of digital paint splatter, but they are starting points, not endpoints. Try combining the watercolor bloom with the drip physics. Feed audio input into the action painting simulator so music drives the gesture. Export your generative compositions as high-resolution PNGs for printing. Or connect a Leap Motion controller and fling virtual paint with your actual hands.
Explore more generative art on Lumitree, where every branch is a unique micro-world built from code. For related topics, see the particle system guide for advanced physics simulation, the fluid simulation tutorial for Navier-Stokes-based liquid effects, or the drawing with code deep-dive for foundational creative coding techniques.