Recursive Steiner Chains

Steiner Chains are a set of N circles where each circle is tangent to 2 other non-intersecting circles as well as the the previous and next circles of the chain, as seen in the below images:

In this challenge, you will write a program/function that draws Steiner chains recursively, that is, circles of a given chain will be the base circles of another iteration of chains:

Challenge

Write a program/function that accepts image dimensions and a list of integers denoting the level of circles in each successive iteration of chains, and output an image with the recursive Steiner chains drawn to it.

Input

Your program/function will accept 2 arguments:

• s - width and height of image
• ls - list of positive integers denoting the number of circles present in each successive iteration of chains, ordered from the top-most chain to the bottom-most chain

Output

Your program/function will output an image of dimension s x s displaying the recusive Steiner chain.

• The top level base circle will be as large as the image with a diameter of s, centered inside the image
• To make things easy, the 2 base circles of a Steiner chain will be concentric, that is, the centerpoints of the 2 baseline circles will be the same
• Given an outer radius, R, and the number of circles in a chain, N, the formula for the inner radius R' is R' = (R-R*sin(pi/N))/(sin(pi/N)+1)
• Circles of the chain as well as the inner base circle will be the outer base circles of the next iteration of chains
• While recursing through the chain circles, the order of the next chain should correspond to the next value in ls
• While recursing through the inner circle of a chain, the order should be the same as its parents order (example [5,2]):
• All chains should end recursion at a depth of the length of ls
• The rotation of the chains doesn't matter:
• However, the rotations of recursive chains relative to their parents centerpoint should be the same:
• All circles should be drawn with an outline or solid fill
• Color choice is left to the implementation, save for loopholes (for example, filling everything with the same color)

Example Runs

In the following examples, color is determined by (depth of the recursion)^4.

You can find source here.

chain(600,[5,4,3])


chain(600,[11,1,1,1,1,1,1])


chain(600,[5,6,7,8,9])


• Related. May 18, 2016 at 18:59

Javascript ES6, 379 bytes

This solution was used to generate the example runs in the question.

f=(s,ls)=>{with(V=document.createElementcanvas)with(getContext2d)with(Math)return(width=height=s,translate(s/=2,s),(S=(o,d=0,n=ls[d],i=(o-o*sin(PI/n))/(sin(PI/n)+1),r=0)=>{fillStyle=rgba(0,0,0,${pow(d/ls.length,4)});beginPath(),arc(0,0,o,-PI,PI),fill();if(d++<ls.length){S(i,d,n);for(;r<n;++r){save();translate(0,(o+i)/2);S((o-i)/2,d);restore();rotate((2*PI)/n);}}})(s),V)}  Ungolfed: f=(s,ls)=>{ // define function that accepts image dimensions and a list of orders with(V=document.createElementcanvas) // create canvas to draw on, bring its functions into current scope chain with(getContext2d) // bring graphics functions into current scope chain with(Math)return( // bring Math functions into current scope chain width=height=s, // set width and height of image translate(s/=2,s), // center the transform on image (S=(o,d=0, // define recursive function that accepts outer radius, depth, and optionally order n=ls[d], // default chain order to corresponding order in input list i=(o-o*sin(PI/n))/(sin(PI/n)+1), // calculate inner base circle radius r=0)=>{ // initialize for loop var fillStyle=rgba(0,0,0,${pow(d/ls.length,4)}); // fill based on depth
beginPath(),arc(0,0,o,-PI,PI),fill();          // draw circle
if(d++<ls.length){                             // if within recursion limit
S(i,d,n);                                     //   recurse on inner circle
for(;r<n;++r){                                //   loop through all circles of the chain
save();                                      //   save transform
translate(0,(o+i)/2);                        //   translate origin to middle of the 2 base circles
S((o-i)/2,d);                                //   recurse on chain circle
restore();                                   //   restore transform
rotate((2*PI)/n);                            //   rotate transform to next circle in chain
}}})(s),                                        // begin the recursion
V)}                                               // return the canvas


Note: f returns a canvas.

Example run (assumes there's a <body> to append to):

document.body.appendChild(f(600,[13,7,11,5,3]))


Should dump the following image to the page:

@Dendrobium

I was trying to port @Dendrobium's Javascript answer in Python.

$$\\color{red}{\text{But the resolution of my fractal image is terrible.}}\$$

from PIL import Image, ImageDraw
import numpy as np
import matplotlib.pyplot as plt

def draw_fractal(s, ls):
# Create a new image with a white background
image = Image.new('RGBA', (s, s), color = (0, 0, 0, 0))
draw = ImageDraw.Draw(image)

# Begin the recursion
draw_fractal_recursive(draw, s/2, s/2, s/2, 0, ls, s)

# Display the image
plt.imshow(np.array(image))
plt.show()

def draw_fractal_recursive(draw, x, y, o, depth, ls, s):
if depth >= len(ls):
return

# Chain order to corresponding order in input list
n = ls[depth]

# Calculate inner base circle radius
i = (o-o*np.sin(np.pi/n))/(np.sin(np.pi/n)+1)

# Fill based on depth
rgba_color = (0, 0, 0, int(255 * np.power(depth/len(ls), 4)))

# Draw circle
draw.ellipse((x-o, y-o, x+o, y+o), fill=rgba_color)

# Recurse on inner circle
draw_fractal_recursive(draw, x, y, i, depth+1, ls, s)

# Loop through all circles of the chain
for r in range(n):
# Translate origin to middle of the 2 base circles
new_x = x + (o+i)/2 * np.cos(2 * np.pi * r / n)
new_y = y + (o+i)/2 * np.sin(2 * np.pi * r / n)

# Recurse on chain circle
draw_fractal_recursive(draw, new_x, new_y, (o-i)/2, depth+1, ls, s)

# Call the function
draw_fractal(600,[13,7,11,5,3])