Creating patterns: How can I create a geometric shape that looks like a rectangle with inward-curved circular arcs replacing the corners?

My goal is to create random patterns like you see it at the top of the image.
I want to build this by using separated shapes like it’s shown in the graphic with the red lines.
So there will be circles, rectangles with rounded corners and shapes like the one at the bottom.

With this shape at the bottom, I am not sure if it is possible to display in Processing.
I couldn’t find a similar topic, but maybe I just searched for the wrong terms:)

I also thought about importing an SVG, but then I am not flexible later when I want to give the circles more space in between or make them smaller.

Is there a way to display inverted rounded corners?
The arcs are symmetrical and connect the sides of the rectangle, creating a concave effect at the corners. Would Bézier curves or another method be best for this?

Thanks for your help!

You can take a look at this code:

void setup() {
  size(400, 400, JAVA2D);
}
void draw() {
  ellipse(20,20,10,10);
  image(generateShape(80,color(0,0,0)), 0, 0);
}
PImage generateShape(int sz, color cl) {
  PGraphics pim;
  pim=createGraphics(sz, sz);
  pim.beginDraw();
  pim.fill(cl);
  pim.rect(0, 0, sz, sz);
  int[] mask=new int[sz*sz];
  for (int i=0; i<sz; i++) for (int j=0; j<sz; j++) if (random(0,10)>4) {//Insert your condition for deletion the pixel at position i,j
    mask[i+sz*j]=0;
  } else mask[i+sz*j]=255;
  pim.mask(mask);
  pim.endDraw();
  return pim;
}

This generates a PGraphics object that contains the shape. You can reuse the shape as often as you want. Just insert the condition of wich pixel to cut out,

Is there a way to display inverted rounded corners?

Have you tried using 4 arcs?

int _wndW = 600;
int _wndH = 600;

void drawArc(float x, float y, float radius, int rotation, float penSize) {
  float angle = 0.0;

  pushMatrix();
  translate(x, y);
  rotate(radians(rotation));
  for ( int i = 0; i < 90; i++ ) {
    angle = radians( i );
    x = cos( angle ) * radius;
    y = sin( angle ) * radius;
    fill(0);
    circle(x, y, penSize); // pen is circle
  }
  popMatrix();
}

void setup() {
  size(_wndW, _wndH);
  background(209);
  drawArc(130, 260, 135, -45, 6);
  drawArc(320, 70, 135, 45, 6);
  drawArc(510, 260, 135, 135, 6);
  drawArc(320, 450, 135, 225, 6);
}

void draw() {
}

Output:

The shape you want to draw has concave sides so although it is easy to draw its contour it is more challenging to fill the shape with colour. There are a number of techniques you can use

  1. Draw the contour and use a flood fill
    Processing does not have a flood fill function so you would need to create your own.
  2. Use a mask image to control which pixels are painted
    You have to create the mask image which poses the same problem.
  3. Define a clipping region to limit pixels to be painted
    Processing (Java mode) only supports rectangular clipping regions

star4

This image was created using the sketch below and demonstrates one technique Ifor complex shapes and could be combined with (2) to provide a more flexible fill for the shape.

PGraphics s0;
int step = 80; // offset between circle centres
int d = step - 6; // circle diameter

void setup() {
  size(400, 400);
  s0 = getStar4(d, color(0, 0, 192));
}

void draw() {
  background(240);
  noStroke();
  fill(0, 0, 128);
  imageMode(CENTER);
  for (int x = step; x < width - step; x += step)
    for (int y = step; y < height - step; y += step) {
      ellipse(x, y, d, d);
      image(s0, x + step/2, y + step/2);
    }
}

PGraphics getStar4(int s, int c) {
  PGraphics pg = createGraphics(s, s);
  int hs = s/2;
  pg.beginDraw();
  pg.clear();
  pg.noStroke();
  pg.fill(0);
  pg.translate(hs, hs);
  pg.ellipse(hs, hs, s, s);
  pg.ellipse(hs, -hs, s, s);
  pg.ellipse(-hs, hs, s, s);
  pg.ellipse(-hs, -hs, s, s);
  pg.endDraw();
  pg.loadPixels();
  int[] a = pg.pixels;
  for (int i = 0; i < a.length; i++)
    a[i] = (a[i] & 0xFF000000) == 0 ? c : 0;
  pg.updatePixels();
  return pg;
}

3 Likes

Thanks quark,

based on your code I now manged to create this, which already brings me a lot closer to my goal.

I tried using masks too as @NumericPrime suggested and feel like it is pretty similar to use.
Using PGraphics for this was a major hint, so thanks for that.

The idea of creating arcs and filling them afterward seems more complicated to me.

reminds me of this:

see Shape generator help (Armin Hofmann's 'rubber band' shape generator)

1 Like

@zwiesabel

I would advise against mixing pixel-based and vector-based shapes. It would be preferable, from both an aesthetic and technical point of view, to remain consistent by limiting yourself to just one of these objects throughout your project.

Having said that, I think you need to go back and think more broadly about the different techniques that are usually associated with this kind of pattern. Only then will you realize that the rhomboid shape you’re so concerned about doesn’t really matter, as it’s usually the mere by-product of a more general logic.

Among the approaches worth investigating are Truchet tiling (specifically, Smith tiles) and metaballs. Although these methods produce patterns slightly different from those shown in your example, they provide excellent starting points for considering possible solutions.

1/ Tiling system

You could certainly design your own tiles and arrange them generatively within a grid, using simple constraint satisfaction solvers or stochastic texture synthesis algorithms (such as WFC or MSTS). Some tiles could represent arcs or squares, while others would depict curved corners. Most of them would have to be flipped and rotated.

The concave rhombus would then be the product of assembling 4 tiles with matching patterns.

As you can imagine, this approach is relatively tedious to set up (tile design + adjacency rules + implementation) and has the added disadvantage of being fairly inflexible (a wider space between the circles or a change in color would require the entire tile set to be redesigned).

2/ Grid layout

A somewhat similar but more flexible approach would involve manipulating geometric shapes directly instead of managing pixelated tiles. Inspired by the grids used in typography to create letters and glyphs, you could create a series of circles aligned in an orthogonal grid and play with the spaces between them to form curved rhombuses or diamonds.

Here, it is the geometric difference between an ellipse and the square cell it occupies that creates curved corners that, when blended together, form a rounded connection between two circles. In a way, this can be likened to the concept of negative space (or closure) in Gestalt theory.

In recent years, there have been many fonts created based on this very basic principle. And logically, they all resemble the patterns you’re trying to reproduce. See for yourself:

Also worth mentioning is this interactive Processing sketch from SchultzSchultz Graphik (2022).

3/ Metaballs

Last but not least. Metaballs are organic shapes formed by blending multiple circles or spheres based on their mutual influence within a scalar field. It generally involves creating an iso-surface, which is computed either by calculating inverse distances (both pixel-based and vector-based) or by using techniques such as Marching Squares or Meandering Triangles (primarly vector-based). Posterization using SVG filters can also produce similar visual effects.

The issue with these approaches is that they are typically limited to points (or circles) within a distance field, making them unsuitable for manipulating geometric elements of varying shapes.

However, there is a visually similar approach that would be particularly suitable for your project and is often underappreciated: the ‘shrink-wrap’ method.
This involves approximating the closing of a set of polygons by inwardly offsetting the outward offset of the entire shape. More specifically, it’s the ‘erosion’ process applied after the preceding ‘dilation’.

Note that the PGS library offers a built-in method specifically designed for this task (see the minimal example provided by its author here).

It’s the simplest workaround I can think of, as it only involves placing circles and rectangles without needing at all to address the connections between them.

Please find below a simple example script written in py5 (a version of Processing for Python 3.9+). The logic is as follows:

  • create a categorical matrix (integers between 0 and 2 - or more)
  • traverse it recursively as an undirected graph
  • cluster adjacent nodes with similar values (Moore neighborhood)
  • create rectangular shapes between each pair of orthogonally connected nodes
  • also create a circle around each visited node
  • compute an approximation of the closing of each set of shapes using PGS_Morphology.dilationErosion()
Anotated Script
"""
Simple script demonstrating how to generate patterns reminiscent of metaballs and of
Smith tiles (a variant of Truchet tiles). 
Illustrate the concept of a "closing" (dilatation / erosion of a set) in comp. geom.
Differences with Truchet patterns: no tilling system, non-periodicity.
Differences with traditional m-balls: no iso-surface, no marching squares, no SVG filters
           
# Author: sol/ub ◐
# Created: 2024-10-05
# Python Version: 3.9
# Music: Brew - Daisuke Miyatani 
# Context: Reply to @zwiesabel -> t.ly/croZv (Processing Discourse Forum) 
           
"""

from collections import defaultdict
from micycle.pgs import *


W, H = 720, 480  # Dimensions of the canvas (W*H) 
X, Y = 6, 4      # Dimensions of the grid/matrix (X*Y)
G, E = 6, 74     # Circle's parameters (gap & extent/diameter)
O    = 42        # Offset distance (outward-inward)

Tx = (W - (E+G) * (X-1)) / 2  # Translation horizontally
Ty = (H - (E+G) * (Y-1)) / 2  # Translation vertically

BG = '#EBEBEB'   # Background color
FG = '#252525'   # Foreground color
SW = 2           # Stroke weight


def setup():
    size(W, H, P2D)
    smooth(8)

    
def draw():
    background(BG)
    translate(Tx, Ty)
    
    # X*Y quaternary matrix (flattened)
    keys = [int(round(random(3))) for i in range(X*Y)]
        
    # Find all islands & populate them with circles + rects
    islands = connect_components(keys, Y, X)

    # Compute the 'closing' (approximation) of each isle
    for isle in islands: 
        shapes = PGS_Conversion.flatten(*isle)
        closing = PGS_Morphology.dilationErosion(shapes, O)
        closing.setStroke(color.Colors.BLACK)
        closing.setStrokeWeight(SW)
        closing.setFill(color.ColorUtils.composeColor(0, 0, 0))
        shape(closing)
        
    no_loop()
        

def mouse_pressed():
    redraw()
    
  
def connect_components(flat_matrix, nrow, ncol):
    
    """
    Traverse a discrete-valued categorical matrix (binary, n-ary) as an undirected
    graph and create rectangular shapes between each pair of orthogonally connected
    components. A circle is also centered around each visited node.
    
    
    Args:
        matrix (1D list): a flattened 2D matrix of integers.
        
    Returns:
        islands (2D list of PShapes): list of rectangles & circles for each island
        
    
    Note: An 'island' refers to a set of connected components made up of adjacent
          nodes (Moore neighborhood) sharing the same value.
    """
                
    # 8 possible directions (Moore neighborhood in anticlockwise order)
    directions = [(1, 0), (1, -1), (0, -1), (-1, -1), (-1, 0), (-1, 1), (0, 1), (1, 1)]
    
    # Define the parameters of a rectangle and the location of an antidiagonal (lambdas)
    # based on the orientation (integer) of a pair of neighboring nodes
    conditions = {0: lambda x, y: (x - (E * 0.5), y, E, E + G),     # downward rectangle (vertically)
                  2: lambda x, y: (x, y - (E * 0.5), -(E + G), E),  # leftward rectangle (horizontally)
                  4: lambda x, y: (x - (E * 0.5), y, E, -(E + G)),  # upward rectangle (vertically)
                  6: lambda x, y: (x, y - (E * 0.5), E + G, E),     # rightward rectangle (horizontally)

                  1: lambda x: (x - 1, x + X),   # left to bottom anti-diagonal   \ ◣
                  3: lambda x: (x - 1, x - X),   # left to top anti-diagonal      / ◤
                  5: lambda x: (x + 1, x - X),   # right to top anti-diagonal     \ ◥
                  7: lambda x: (x + 1, x + X)}   # right to bottom anti-diagonal  / ◢

                
    visited_neighbors = defaultdict(set)
    nodes_to_visit = set(range(len(flat_matrix)))
    
    
    def dfs(i, shapes=None):
        
        # if this is the first non-recursive call -> initialize 'shapes'
        if shapes is None:
            shapes = []
            
        # else -> mark the current node as 'visited'
        else:
            nodes_to_visit.remove(i)
        
        # get row and column for the current index
        row, col = divmod(i, ncol)
        
        # get the corresponding coordinates
        x = (col*E)+(col*G)
        y = (row*E)+(row*G)
        
        # create a circle centered around the current node & add it to the bag-of-shapes
        c = create_shape(ELLIPSE, x, y, E, E)
        shapes.append(c)
        
        # for each direction:
        for j, (dr, dc) in enumerate(directions):
            
            # calculate the row and column of the neighbor
            nr, nc = row + dr, col + dc
            
            # check if the neighbor is inside the grid bounds
            if 0 <= nr < nrow and 0 <= nc < ncol:
                
                # convert 2D coordinates back to 1D index
                ni = nr * ncol + nc
                
                # if the neighbor shares the same value and this pair hasn't been visisted yet:
                if flat_matrix[ni] == flat_matrix[i] and ni not in visited_neighbors[i]:
                    
                    # if the pair is orthogonally aligned:
                    if not j&1:
                        
                        # -> create a rectangle between them & add it to the bag-of-shapes
                        r = create_shape(RECT, *conditions[j](x, y))
                        shapes.append(r)
                    
                    # if the pair is aligned diagonally:
                    else:
                        
                        # -> discard the connection if it crosses another perpendicularly
                        if all(pos not in nodes_to_visit for pos in conditions[j](i)):
                            continue      

                    # mark the pair as 'visited'
                    visited_neighbors[i].add(ni)
                    visited_neighbors[ni].add(i)
                    
                    # if the neighbor node hasn't been visited yet:
                    if ni in nodes_to_visit:
                        
                        # -> visit it recursively
                        dfs(ni, shapes)
                        
        return shapes                    
          
          
    # Find all islands & populate them with shapes until every node has been visited
    islands = []
    while nodes_to_visit:
        islands.append(dfs(nodes_to_visit.pop()))
    
    return islands

panel

6 Likes