There are different ways to achieve that organic look you’re looking for but I’m afraid that noising a circle isn’t the best option.
The edges on your example sketch do look “smooth” (no need to use bezierVertex()
) but the reason you’re not satisfied with it is because the height around the center is lacking contrast / variation. Based on the picture you provided I’d say that “splatter effect” is what you’re actually after.
Still, we can try to change your sketch a bit. Modifying the noise function and do some scaling will somewhat improve both the motion and the shape of the circle:
see motion here
(please note that I’m using Python mode but it should be really easy to port it to Java)
n_points, radius = 200, 300
angle = radians(360) / n_points
factor = .2
def setup():
size(800, 600, P2D)
noStroke()
smooth(8)
beginShape() #Avoid calling beginShape() at each iteration
def draw():
background(255)
translate(width>>1, height>>1)
for e in range(n_points+1):
x = cos(angle * e) * radius
y = sin(angle * e) * radius
p = PVector(x, y).normalize()
n = map(noise(p.x * factor + frameCount*.01, p.y * factor + frameCount*.01), 0, 1, 10, 200)
p.mult(n)
fill(100 - e / 10 - abs(p.y), (90 + e) - (p.y/4), 220)
vertex(p.x, p.y)
endShape(CLOSE)
In order to get a more blob looking shape I would suggest different approaches:
1/ Iso-surface (Metaballs)
Daniel Shiffman did a coding challenge on Metaballs a while ago. You can implement your own distance field based on his code or you can chose to use a library instead. I’ve found Computational Geometry to be one of the easiest to use for this purpose.
(see the bunddled examples for Java sketches)
add_library('ComputationalGeometry')
def setup():
size(800, 500, P3D)
rectMode(CENTER)
background(255)
noStroke()
smooth(8)
iso1 = IsoContour(this, PVector(), PVector(width, height), 200, 125) #last 2 int are the number of columns and rows. The higher, the better the resolution
iso2 = IsoContour(this, PVector(), PVector(width, height), 200, 125)
thresh = .0004 #the lower, the bigger the blobs
for i in range(20):
p1 = PVector(random(50, width - 50), random(height), 0)
p2 = PVector(random(50, width - 50), random(height), 0)
iso1.addPoint(p1)
if i> 5: iso2.addPoint(p2)
fill('#002d6b')
rect(width>>1, height>>1, width - 120, height)
fill('#e01a2c')
iso1.plot(thresh)
fill('#ffc20e')
iso2.plot(thresh)
2 majors drawbacks:
- because of the need to compute distances from each point on the grid to the ball (and this, for each ball), the algorithm is computationally expensive. Hence the difficulty to make an animation, unless you decide to implement it in a shader.
- because it is making use of the whole array of pixels (
loadPixels
/ updatePixels
in its naive implementation) it is not possible to select the color of a specific ball.
2/ “Gooey Effect”
Visually similar to metaballs, it is a technique coming from the Web design community that consists in applying SVG filters (gaussian blur + high contrast in the alpha channel) on moving ellipses in order to get blob-like motions.
Example of a gooey effect implemented in three.js by Misaki Nakano. See it live here
Although the melting effect doesn’t look as good as in the original Javascript version, noahbuddy from the forum did a fantastic job trying to port that technique in Processing with a shader.
(See original Java version here)
def setup():
global buf, balls, contrast, blurry
size(1240, 720, OPENGL)
rectMode(CENTER)
noStroke()
smooth(8)
buf = createGraphics(width, height, P2D)
contrast = loadShader("colFrag.glsl")
blurry = loadShader("blurFrag.glsl")
blurry.set("sigma", 10.5)
blurry.set("blurSize", 30)
balls = [Ball() for e in range(40)]
def draw():
background(255)
fill('#002d6b')
rect(width>>1, height>>1, width - 120, height)
buf.beginDraw()
buf.background(190, 0)
buf.noStroke()
for b in balls:
b.update()
b.render()
blurry.set("horizontalPass", 1)
buf.filter(blurry)
blurry.set("horizontalPass", 0)
buf.filter(blurry)
buf.endDraw()
shader(contrast)
image(buf, 0, 0, width, height)
class Ball(object):
def __init__(self):
self.loc = PVector(random(width), random(height), 0)
self.vel = PVector.random2D()
self.radius = random(60, 140)
self.c = [[224,26,44], [255,194,14]]
self.r = int(random(2))
def update(self):
self.loc.add(self.vel)
if self.loc.x > width or self.loc.x < 0: self.vel.x *= -1
if self.loc.y > height or self.loc.y < 0: self.vel.y *= -1
def render(self):
buf.fill(self.c[self.r][0], self.c[self.r][1], self.c[self.r][2])
buf.ellipse(self.loc.x, self.loc.y, self.radius, self.radius)
colFrag.glsl
#define PROCESSING_TEXTURE_SHADER
uniform sampler2D texture;
varying vec4 vertTexCoord;
uniform vec4 o = vec4(0, 0, 0, -8.0);
uniform lowp mat4 colorMatrix = mat4(1.0, 0.0, 0.0, 0.0,
0.0, 1.0, 0.0, 0.0,
0.0, 0.0, 1.0, 0.0,
1.0, 1.0, 1.0, 8.0);
void main() {
vec4 pix = texture2D(texture, vertTexCoord.st);
vec4 color = (pix * colorMatrix) * 1.15 + o ;
gl_FragColor = color;
}
blurFrag.glsl
// Adapted from:
// <a href="http://callumhay.blogspot.com/2010/09/gaussian-blur-shader-glsl.html" target="_blank" rel="nofollow">http://callumhay.blogspot.com/2010/09/gaussian-blur-shader-glsl.html</a>
#ifdef GL_ES
precision mediump float;
precision mediump int;
#endif
#define PROCESSING_TEXTURE_SHADER
uniform sampler2D texture;
// The inverse of the texture dimensions along X and Y
uniform vec2 texOffset;
varying vec4 vertColor;
varying vec4 vertTexCoord;
uniform int blurSize;
uniform int horizontalPass; // 0 or 1 to indicate vertical or horizontal pass
uniform float sigma; // The sigma value for the gaussian function: higher value means more blur
// A good value for 9x9 is around 3 to 5
// A good value for 7x7 is around 2.5 to 4
// A good value for 5x5 is around 2 to 3.5
// ... play around with this based on what you need <span class="Emoticon Emoticon1"><span>:)</span></span>
const float pi = 3.14159265;
void main() {
float numBlurPixelsPerSide = float(blurSize / 2);
vec2 blurMultiplyVec = 0 < horizontalPass ? vec2(1.0, 0.0) : vec2(0.0, 1.0);
// Incremental Gaussian Coefficent Calculation (See GPU Gems 3 pp. 877 - 889)
vec3 incrementalGaussian;
incrementalGaussian.x = 1.0 / (sqrt(2.0 * pi) * sigma);
incrementalGaussian.y = exp(-0.5 / (sigma * sigma));
incrementalGaussian.z = incrementalGaussian.y * incrementalGaussian.y;
vec4 avgValue = vec4(0.0, 0.0, 0.0, 0.0);
float coefficientSum = 0.0;
// Take the central sample first...
avgValue += texture2D(texture, vertTexCoord.st) * incrementalGaussian.x;
coefficientSum += incrementalGaussian.x;
incrementalGaussian.xy *= incrementalGaussian.yz;
// Go through the remaining 8 vertical samples (4 on each side of the center)
for (float i = 1.0; i <= numBlurPixelsPerSide; i++) {
avgValue += texture2D(texture, vertTexCoord.st - i * texOffset *
blurMultiplyVec) * incrementalGaussian.x;
avgValue += texture2D(texture, vertTexCoord.st + i * texOffset *
blurMultiplyVec) * incrementalGaussian.x;
coefficientSum += 2.0 * incrementalGaussian.x;
incrementalGaussian.xy *= incrementalGaussian.yz;
}
gl_FragColor = avgValue / coefficientSum;
}
3/ Physics (springs + repulsion behaviors)
Another possibilty would be to connect particles (displayed around a circle) with springs and give them a repulsion force from each other that is proportional to distance.
(blob by Zach Lieberman, taken from this post)
see motion here
I remember trying to implement it with the toxiclibs library but failed eventually because it wa impossible to give particles an individual repulsion force that would target another specific particle. So, if you decide go down that road I would suggest to code your own physics engine from scratch (check these videos on attractors and springs from Daniel Shiffman).
4/ Differential Growth
Probably the more elaborate solution but mentionning it here just for brainstroming. It starts the same way (particles with repulsion force connected by springs around a circle) except new cells (particles) are randomly added between pre-existing cells as the sketch is iterating. Very quickly (during the early stages of the growth) the circle turns into a blob like shape with that splatter effect you’re looking for:
Check the fantastic tutorials from Satoru Sugihara for more info.
Drawbacks:
- can be tricky to implement (managing / updating the connections)
- the order of the cells needs to be re-computed every time a cell division occurs (only if you want to fill the shape)
- becomes slow at some point