Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

canvas "random" curved shapes

I want to draw random-looking curved blobs on a canvas, but I can't seem to come up with an algorithm to do it. I've tried creating random Bezier curves like this:

context.beginPath();
// Each shape should be made up of between three and six curves
var i = random(3, 6);
var startPos = {
    x : random(0, canvas.width),
    y : random(0, canvas.height)
};
context.moveTo(startPos.x, startPos.y);
while (i--) {
    angle = random(0, 360);
    // each line shouldn't be too long
    length = random(0, canvas.width / 5);
    endPos = getLineEndPoint(startPos, length, angle);
    bezier1Angle = random(angle - 90, angle + 90) % 360;
    bezier2Angle = (180 + random(angle - 90, angle + 90)) % 360;
    bezier1Length = random(0, length / 2);
    bezier2Length = random(0, length / 2);
    bezier1Pos = getLineEndPoint(startPos, bezier1Length, bezier1Angle);
    bezier2Pos = getLineEndPoint(endPos, bezier2Length, bezier2Angle);
    context.bezierCurveTo(
        bezier1Pos.x, bezier1Pos.y
        bezier2Pos.x, bezier2Pos.y
        endPos.x, endPos.y
    );
    startPos = endPos;
}

(This is a simplification... I added bits constraining the lines to within the canvas, etc.) The problem with this is getting it to head back to the starting point, and also not just making loads of awkward corners. Does anyone know of a better algorithm to do this, or can think one up?

Edit: I've made some progress. I've started again, working with straight lines (I think I know what to do to make them into smooth Beziers once I've worked this bit out). I've set it so that before drawing each point, it works out the distance and angle to the start from the previous point. If the distance is less than a certain amount, it closes the curve. Otherwise the possible angle narrows based on the number of iterations, and the maximum line length is the distance to the start. So here's some code.

start = {
    // start somewhere within the canvas element
    x: random(canvas.width),
    y: random(canvas.height) 
};
context.moveTo(start.x, start.y);
prev = {};
prev.length = random(minLineLength, maxLineLength);
prev.angle = random(360);
prev.x = start.x + prev.length * Math.cos(prev.angle);
prev.y = start.y + prev.length * Math.sin(prev.angle);

j = 1;

keepGoing = true;
while (keepGoing) {
    j++;
    distanceBackToStart = Math.round(
        Math.sqrt(Math.pow(prev.x - start.x, 2) + Math.pow(prev.y - start.y, 2)));
    angleBackToStart = (Math.atan((prev.y - start.y) / (prev.x - start.x)) * 180 / Math.pi) % 360;
    if (isNaN(angleBackToStart)) {
        angleBackToStart = random(360);
    }
    current = {};
    if (distanceBackToStart > minLineLength) {
        current.length = random(minLineLength, distanceBackToStart);
        current.angle = random(angleBackToStart - 90 / j, angleBackToStart + 90 / j) % 360;
        current.x = prev.x + current.length * Math.cos(current.angle);
        current.y = prev.y + current.length * Math.sin(current.angle);
        prev = current;
    } else {
        // if there's only a short distance back to the start, join up the curve
        current.length = distanceBackToStart;
        current.angle = angleBackToStart;
        current.x = start.x;
        current.y = start.y;
        keepGoing = false;
    }
    context.lineTo(current.x, current.y);
}
console.log('Shape complexity: ' + j);
context.closePath();
context.fillStyle = 'black';
context.shadowColor = 'black';
context.shadowOffsetX = -xOffset;
context.shadowOffsetY = -yOffset;
context.shadowBlur = 50;
context.fill();

The problem I've got now is that the shape's outline often crosses over itself, which looks wrong. The only way I can think of to solve this is to keep track of a bounding box, and each new point should always head out of the bounding box. That's tricky because calculating the available angle adds a whole level of complexity.

like image 205
Nathan MacInnes Avatar asked Dec 08 '11 12:12

Nathan MacInnes


1 Answers

One possibility would be to use polar coordinates, and have the radius be a function of the angle. For smooth blobs you want the radius to be smooth, and have the same value at 0 and 2*pi, which can be done using a trigonometric polynomial :

radius(theta) = a_0 + a_1*sin(theta) + a_2*sin(2*theta) + ... + b_1*cos(theta) + ...

where the coefficients are "random". To control how big and small the radius gets you could search for the max and min of the radius function, and then shift and scale the coefficients appropriately (ie if you want rlo<=r<=rhi, and have found min and max, then replace each coefficient a + b*original, where b = (rhi-rlo)/(max-min) and a = rlo-b*min).

like image 88
dmuir Avatar answered Oct 20 '22 19:10

dmuir