# Help with smoothly connecting two circles

**URL:** <https://discourse.processing.org/t/help-with-smoothly-connecting-two-circles/46549>\
**Category:** Coding Questions\
**Created:** [June 15, 2025, 4:36am UTC](https://discourse.processing.org/t/help-with-smoothly-connecting-two-circles/46549 "2025-06-15T04:36:47Z")\
**Posts on this page:** 6\
**Page:** 1

<div class="post-metadata">

**Author:** ![arkwl](https://avatars.discourse-cdn.com/v4/letter/a/a3d4f5/32.png) [@arkwl](https://discourse.processing.org/u/arkwl)\
**Post date:** [June 15, 2025, 4:36am UTC](https://discourse.processing.org/t/help-with-smoothly-connecting-two-circles/46549/1 "2025-06-15T04:36:47Z")

</div>

Hello! My goal is to write a function that can organically connect two circles no matter the distance. I originally tried metaballs but found they don’t connect at large distances without increasing the threshold (which changes circle radiuses as a side effect).

So instead, I tried Bézier curves to connect the two circles. I’m limiting the case to (2, 13) and (2, 0). The below code is close but the curve is not perfectly smooth. Please let me know how I can make this curve smoother programmatically & what other techniques I can try.

```auto
void setup() {
  size(400, 400);
  noFill();
  stroke(0);
  
  //fill(255, 0, 0);
  //stroke(255, 0, 0);
}

void draw() {
  background(255);
  translate(width/2, height/2);

  PVector c1 = new PVector(2, 13);
  PVector c2 = new PVector(2, 0);
  float r1 = 5;
  float r2 = 2;

  float scale = 10;
  connectCircles(PVector.mult(c1, scale), r1 * scale, PVector.mult(c2, scale), r2 * scale);

  ellipse(c1.x * scale, c1.y * scale, r1 * 2 * scale, r1 * 2 * scale);
  ellipse(c2.x * scale, c2.y * scale, r2 * 2 * scale, r2 * 2 * scale);
}

void connectCircles(PVector c1, float r1, PVector c2, float r2) {
  PVector dir = PVector.sub(c2, c1);
  float d = dir.mag();
  println(d);
  if (d == 0) return;

  float angle = atan2(dir.y, dir.x);

  // Calculate offset angle from center line to tangent points
  float u = acos(constrain((r1 - r2) / d, -1, 1));
  float angle1 = angle + u;
  float angle2 = angle - u;

  // Edge points
  PVector p1 = c1.copy().add(PVector.fromAngle(angle1).mult(r1));
  PVector p2 = c2.copy().add(PVector.fromAngle(angle1).mult(r2));
  PVector p3 = c2.copy().add(PVector.fromAngle(angle2).mult(r2));
  PVector p4 = c1.copy().add(PVector.fromAngle(angle2).mult(r1));

  // This is the tweak: offset control points slightly along the normal to "cinch" the waist
  float offset = 0.8 * min(r1, r2); // tweakable

  beginShape();
  vertex(p1.x, p1.y);
  bezierVertex(p1.x - offset, p1.y - offset, p2.x - offset, p2.y + offset, p2.x, p2.y);
  vertex(p3.x, p3.y);
  bezierVertex(p3.x + offset, p3.y + offset, p4.x + offset, p4.y - offset, p4.x, p4.y);
  endShape(CLOSE);
  noLoop();
}

```

 ![Screenshot 2025-06-14 at 9.24.55 PM](https://canada1.discourse-cdn.com/flex036/uploads/processingfoundation1/original/3X/c/8/c879159f511c14c7055f4046d8367b70ce268320.png)

---

<div class="post-metadata">

**Author:** ![svan](https://avatars.discourse-cdn.com/v4/letter/s/82dd89/32.png) [@svan](https://discourse.processing.org/u/svan)\
**Post date:** [June 15, 2025, 5:05am UTC](https://discourse.processing.org/t/help-with-smoothly-connecting-two-circles/46549/2 "2025-06-15T05:05:00Z")

</div>

You might be able to get some help by reading this thread: [Shape generator help (Armin Hofmann's 'rubber band' shape generator)](https://discourse.processing.org/t/shape-generator-help-armin-hofmanns-rubber-band-shape-generator/33190)

---

<div class="post-metadata">

**Author:** ![quark](https://yyz2.discourse-cdn.com/flex036/user_avatar/discourse.processing.org/quark/32/26_2.png) [@quark](https://discourse.processing.org/u/quark)\
**Post date:** [June 15, 2025, 5:59pm UTC](https://discourse.processing.org/t/help-with-smoothly-connecting-two-circles/46549/4 "2025-06-15T17:59:15Z")

</div>

To get a smooth transition from a straight line to a circle the line needs to be a tangent so that seems to be a great place to start.

Now for 2 non-intersecting circles there will be 4 lines that are mutually tangential to both circles so calculating the tangent points on the circumferences will allow you to find these straight lines. Then try and connect these points with Bezier curves by calculating appropriate intermediate control points.

This [web page](http://www.lagers.org.uk/gallery/gy-geom-1/gy-index.html) (go to page 3) in the book shows how to calculate the tangent points.

---

<div class="post-metadata">

**Author:** ![quark](https://yyz2.discourse-cdn.com/flex036/user_avatar/discourse.processing.org/quark/32/26_2.png) [@quark](https://discourse.processing.org/u/quark)\
**Post date:** [June 15, 2025, 9:55pm UTC](https://discourse.processing.org/t/help-with-smoothly-connecting-two-circles/46549/5 "2025-06-15T21:55:09Z")

</div>

I have created a small demo in p5js (JavaScript) that demonstrates the technique I described in my last post. It is not perfect but I think it can be _modified_ to suit your purpose.

The small circle will follow the mouse position.

https://editor.p5js.org/quark-js/full/9iYMGpw4J

---

<div class="post-metadata">

**Author:** ![glv](https://yyz2.discourse-cdn.com/flex036/user_avatar/discourse.processing.org/glv/32/18785_2.png) [@glv](https://discourse.processing.org/u/glv)\
**Post date:** [June 15, 2025, 11:00pm UTC](https://discourse.processing.org/t/help-with-smoothly-connecting-two-circles/46549/6 "2025-06-15T23:00:07Z")

</div>

Hello @arkwl ,

Having some fun with your code:

```auto
  float rot = map(mouseX, 0, width/3, 0, TAU/8);

  // Edge points
  PVector p1 = c1.copy().add(PVector.fromAngle(angle1).mult(r1).rotate(-rot));
  PVector p2 = c2.copy().add(PVector.fromAngle(angle1).mult(r2).rotate(+rot));
  PVector p3 = c2.copy().add(PVector.fromAngle(angle2).mult(r2));
  PVector p4 = c1.copy().add(PVector.fromAngle(angle2).mult(r1));

```

 ![image](https://canada1.discourse-cdn.com/flex036/uploads/processingfoundation1/original/3X/a/e/ae2bde1d7aa34f5cfb1da10382e9355e1c28730c.png)

It look much better with P2D on my PC.

`:)`

---

<div class="post-metadata">

**Author:** ![solub](https://yyz2.discourse-cdn.com/flex036/user_avatar/discourse.processing.org/solub/32/333_2.png) [@solub](https://discourse.processing.org/u/solub)\
**Post date:** [June 16, 2025, 10:33pm UTC](https://discourse.processing.org/t/help-with-smoothly-connecting-two-circles/46549/7 "2025-06-16T22:33:51Z")

</div>

Hi @arkwl,

A visually similar approach would be to approximate the [closing](https://en.wikipedia.org/wiki/Closing_(morphology)) of the **pair** of circles by _inwardly offsetting its outward offset_. This eliminates the need to extract arc segments and connect them to the input circles.

Please refer to this [post](https://discourse.processing.org/t/help-creating-organic-looking-blobs/8777/11) for a quick overview and to part 3 of this other [post](https://discourse.processing.org/t/creating-patterns-how-can-i-create-a-geometric-shape-that-looks-like-a-rectangle-with-inward-curved-circular-arcs-replacing-the-corners/45111/7) (entitled “metaballs”) for further explanations.

The main challenge in your case is to dynamically adjust the offset value to ensure the closing of the two circles remains valid regardless of their distance.

If this value is too low, the closing cannot be formed (the two parts of the blob remain disconnected), and if it is too high, the closing will extend beyond the circles’ boundaries.

 ![ex2](https://canada1.discourse-cdn.com/flex036/uploads/processingfoundation1/original/3X/5/0/506cfd134769b5aec871716cc4a865bcb833ffdc.png)

One way of calculating a minimum offset value is to approximate the radius of the arcs that are externally tangent to the two circles (bitangent arcs), with the additional constraint that these arcs _should not intersect each other_.

 ![ex](https://canada1.discourse-cdn.com/flex036/uploads/processingfoundation1/original/3X/f/7/f7d4dec82920050b7eddb3a63370961875b68ae4.png)

The intuition is that these bitangent arcs naturally define the minimum curvature needed to smoothly connect the circles while maintaining their original shapes. By using this radius as our offset value, we ensure that the closing operation will always produce a valid, organic connection between the circles, regardless of their distance.

To compute a valid minimum radius for the bitangent arc circles, the procedure is as follows:

- find the minimum radius R that makes the arcs tangent to both circles
- use the [Heron’s formula](https://en.wikipedia.org/wiki/Heron%27s_formula) to locate the centers of the circular arcs
- if the arc circles are colliding, iteratively increase R by a small factor until they are properly separated
- compute an approximation of the _closing_ of the pair of circles based on that updated R value using `PGS_Morphology.dilationErosion()`

![gooey](https://canada1.discourse-cdn.com/flex036/uploads/processingfoundation1/original/3X/9/c/9c3adc7353df79f2d323dfd67fa8b52f937f1827.gif)

_Please note that I’ve added a mechanism that linearly increases the arc radius as the distance between the circles decreases. This creates a more organic, gooey feel to the connection - as if the circles were made of a sticky, elastic material that stretches more when they’re far apart._

> **Annotated Script (py5)**
>
> ```auto
> """
> Example script demonstrating how to create an organic connection between two circles
> using morphological operations (dilation and erosion). The offset value is calculated
> by finding two bitangent arcs between the circles. The arcs are guaranteed to be tangent
> to both circles and will not intersect each other. The script handles various cases
> including intersecting circles and provides a mechanism to control the separation
> between the arcs.
>            
> # Author: sol/ub ◐
> # Created: 2025-17-06
> # Python Version: 3.9
> # Context: Reply to @arkwl -> t.ly/5tipm (Processing Discourse Forum) 
>            
> """
> 
> from micycle.pgs import *
> from org.locationtech.jts.operation.buffer import BufferOp, BufferParameters
> import math
> 
> W, H = 1400, 800 # Dimensions of canvas
> 
> R1 = 40 # Radius of the first circle
> R2 = 160 # Radius of the second circle
> C2x, C2y = W//2, H//2 # Coordinates of the second circle
> 
> SW = 2 # Stroke weight
> BG = '#FFF' # Background color
> 
> # Define high-resolution buffer parameters (PGS related)
> NS = 32 # increase for smoother contour
> params = BufferParameters()
> params.setQuadrantSegments(NS)     
> 
> def setup():
> size(W, H, P2D)
> smooth(8)
>     
> global c2
>     
> # Set the second circle at the center of the canvas
> c2 = create_shape(ELLIPSE, C2x, C2y, R2*2, R2*2)
>     
> def draw():
> background(BG)
>     
> # Set the first ellipse at mouse position
> c1 = create_shape(ELLIPSE, mouse_x, mouse_y, R1*2, R1*2)
>             
> # Calculate a minimum tangent arc radius (= offset value for the dilatation/erosion process)
> R = find_min_radius(mouse_x, mouse_y, R1, C2x, C2y, R2)
>     
> # Convert the circles to JTS Geometry
> shapes = PGS_Conversion.flatten(*(c1, c2))
> geom = PGS_Conversion.fromPShape(shapes)
>     
> # Dilate & erode
> dilated = BufferOp.bufferOp(geom, R, params) # First buffer (dilation)
> closed = BufferOp.bufferOp(dilated, -R, params) # Second buffer (erosion)
>     
> # Convert the closing back to PShape
> closing = PGS_Conversion.toPShape(closed)
>     
> # Set colors and stroke weight
> closing.setStroke(color.Colors.BLACK)
> closing.setStrokeWeight(SW)
> closing.setFill(color.ColorUtils.composeColor(235, 238, 240))
> shape(closing)
>         
> 
> def find_min_radius(x1, y1, r1, x2, y2, r2):
>     
> # Calculate distance between circle centers
> dx = x2 - x1
> dy = y2 - y1
> d = math.sqrt(dx*dx + dy*dy)
> theta = math.atan2(dy, dx)
>     
> extra_separation = constrain(remap(d, R2-R1, W*.8, 600, 1), 1, 600) # personalized values to be modified according to your preferences
>     
> # Check if one circle is completely inside the other
> if d <= abs(r1 - r2):
> #raise ValueError("One circle is completely inside the other")
> return 0
>     
> # Check if circles intersect
> if d < r1 + r2:
> # Calculate intersection points
> # Using the formula from: https://mathworld.wolfram.com/Circle-CircleIntersection.html
> d2 = d * d
> r12 = r1 * r1
> r22 = r2 * r2
>         
> # Calculate the distance from circle 1's center to the intersection line
> a = (r12 - r22 + d2) / (2 * d)
>         
> # Calculate the distance from the intersection line to the intersection points
> h = math.sqrt(r12 - a * a)
>         
> # Calculate the intersection points
> x3 = x1 + a * dx / d
> y3 = y1 + a * dy / d
>         
> # Calculate the perpendicular vector
> perp_x = -dy / d
> perp_y = dx / d
>         
> # Calculate the two intersection points
> int1_x = x3 + h * perp_x
> int1_y = x3 + h * perp_y
> int2_x = x3 - h * perp_x
> int2_y = x3 - h * perp_y
>         
> # Calculate the radius of the tangent arcs
> # The radius should be large enough to pass through both intersection points
> # and be tangent to both circles
> R = max(r1, r2) + extra_separation
>         
> # Calculate the centers of the tangent arcs
> # They should be equidistant from both intersection points
> mid_x = (int1_x + int2_x) / 2
> mid_y = (int1_y + int2_y) / 2
>         
> # Calculate the direction vector between intersection points
> dir_x = int2_x - int1_x
> dir_y = int2_y - int1_y
> dir_len = math.sqrt(dir_x * dir_x + dir_y * dir_y)
> dir_x /= dir_len
> dir_y /= dir_len
>         
> # Calculate the perpendicular distance from midpoint to arc centers
> # Using the formula: R^2 = (d/2)^2 + h^2, where d is distance between intersection points
> h = math.sqrt(R * R - (dir_len/2) * (dir_len/2))
>         
> # Calculate the arc centers
> center1_x = mid_x - h * dir_y
> center1_y = mid_y + h * dir_x
> center2_x = mid_x + h * dir_y
> center2_y = mid_y - h * dir_x
>         
> return R
>     
> # If circles don't intersect, use the original method
> # Calculate initial R for tangency
> R = (d*d - (r1 + r2)*(r1 + r2)) / (4 * (r1 + r2))
>     
> # Calculate arc centers
> def calculate_centers(R):
> a, b, c = (r1 + R), (r2 + R), d
> s = (a + b + c) / 2
>         
> # Calculate area using Heron's formula
> try:
> area = math.sqrt(s * (s - a) * (s - b) * (s - c))
> except ValueError:
> # If we get here, the triangle inequality doesn't hold
> # This shouldn't happen with the checks above, but just in case
> raise ValueError("Invalid triangle configuration")
>             
> h = 2 * area / c
>         
> # If h is too large, we need to adjust R
> if h >= a:
> # Calculate maximum possible h for current R
> h = a * 0.99 # Use 99% of a to ensure we're within bounds
>         
> x = math.sqrt(a*a - h*h)
>         
> center1_x = x1 + x * math.cos(theta) - h * math.sin(theta)
> center1_y = y1 + x * math.sin(theta) + h * math.cos(theta)
> center2_x = x1 + x * math.cos(theta) + h * math.sin(theta)
> center2_y = y1 + x * math.sin(theta) - h * math.cos(theta)
>         
> return (center1_x, center1_y), (center2_x, center2_y)
>     
> # Calculate initial positions
> center1, center2 = calculate_centers(R)
>     
> # Check if arcs are too close
> arc_dx = center2[0] - center1[0]
> arc_dy = center2[1] - center1[1]
> arc_d = math.sqrt(arc_dx*arc_dx + arc_dy*arc_dy)
>     
> # If arcs are too close, increase R until they're properly separated
> target_separation = 2 * R + extra_separation
>     
> if arc_d < target_separation:
> max_attempts = 100 # Prevent infinite loops
> attempts = 0
> while arc_d < target_separation and attempts < max_attempts:
> R *= 1.1 # Increase R by 10%
> center1, center2 = calculate_centers(R)
>             
> # Calculate new distance between centers
> arc_dx = center2[0] - center1[0]
> arc_dy = center2[1] - center1[1]
> arc_d = math.sqrt(arc_dx*arc_dx + arc_dy*arc_dy)
> attempts += 1
>     
> return R
> 
> ```
