Here is my problem:
Perhaps some application of force-directed layout would be useful.
Since your goal is just to "achieve a pleasing effect", not solve a math problem, you should try the simplest algorithm which could work first and see if it looks good. There should be no need to use very complex math.
I understand that you want the spheres to "fill" the available space, not leaving large empty areas while other areas are crowded. You also want the layout to appear random -- not lined up on a grid or anything like that.
The obvious, dead-simple way to achieve that is simply to place the spheres one by one, in random locations. If one lands on top of an already-placed sphere, generate another random location until you find one where it fits.
It appears there are about 40 spheres in the image shown. The chances of 40 spheres all landing in the same area of the image, leaving the rest of the image empty, is very, very small. As the number of spheres increases, the chances of getting a very unbalanced layout will become close to zero.
Try that first and see if it meets your needs. If it is not "even" enough, you should be able to use some very simple math to bias the randomly chosen locations in favor of choosing empty areas. There should be no need to use complex algorithms.
I would try to insert sphere after sphere (largest first). Each one is added in the largest available space, with some random jitter.
One relatively easy way to find (more or less) the largest available space, is to imagine a grid of points on your view and store for each grid point (in a 2D array) the closest distance to any item: edge or sphere, whichever is closest. This array is updated as each new sphere is added.
To add a new sphere, just take the grid point with highest distance and apply some random jitter (you actually know how much you can jitter, because you know the distance to the closest item). (I would randomize not more than (d-r)/2 where d is the distance in the array and r is the radius of the sphere to add.
Updating this array after adding another circle is no rocket science: you calculate for each grid point the distance to newly added sphere and replace the stored value if that was larger.
It is possible that your grid is too coarse, and you can't add any more circle (when the 2D array contains no distances larger than the radius of the circle to add). Then you have to increase (e.g. double) the grid resolution before continuing.
Here are some result of this implementation (it took me about 100 lines of code)
And here is some rough C++ code (just the algorithm, don't expect this to compile)
// INITIALIZATION
// Dimension of canvas
float width = 768;
float height = 1004;
// The algorithm creates a grid on the canvas
float gridSize=10;
int gridColumns, gridRows;
float *dist;
void initDistances()
{
// Determine grid dimensions and allocate array
gridColumns = width/gridSize;
gridRows = height/gridSize;
// We store a 2D array as a 1D array:
dist = new float[ gridColumns * gridRows ];
// Init dist array with shortest distances to the edges
float y = gridSize/2.0;
for (int row=0; row<gridRows; row++)
{
float distanceFromTop = y;
float distanceFromBottom = height-y;
for (int col=0; col<gridColumns; col++)
{
int i = row*gridColumns+col;
dist[i]=(distanceFromTop<distanceFromBottom?distanceFromTop:distanceFromBottom);
}
y+=gridSize;
}
float x = gridSize/2.0;
for (int col=0; col<gridColumns; col++)
{
float distanceFromLeft = x;
float distanceFromRight = width-x;
for (int row=0; row<gridRows; row++)
{
int i = row*gridColumns+col;
if (dist[i]>distanceFromLeft) dist[i] = distanceFromLeft;
if (dist[i]>distanceFromRight) dist[i] = distanceFromRight;
}
x+=gridSize;
}
}
void drawCircles()
{
for (int circle = 0; circle<getNrOfCircles(); circle++)
{
// We assume circles are sorted large to small!
float radius = getRadiusOfCircle( circle );
// Find gridpoint with largest distance from anything
int i=0;
int maxR = 0;
int maxC = 0;
float maxDist = dist[0];
for (int r=0; r<gridRows; r++)
for (int c=0; c<gridColumns; c++)
{
if (maxDist<dist[i]) {
maxR= r; maxC= c; maxDist = dist[i];
}
i++;
}
// Calculate position of grid point
float x = gridSize/2.0 + maxC*gridSize;
float y = gridSize/2.0 + maxR*gridSize;
// Apply some random Jitter
float offset = (maxDist-radius)/2.0;
x += (rand()/(float)RAND_MAX - 0.5) * 2 * offset;
y += (rand()/(float)RAND_MAX - 0.5) * 2 * offset;
drawCircle(x,y,radius);
// Update Distance array with new circle;
i=0;
float yy = gridSize/2.0;
for (int r=0; r<gridRows; r++)
{
float xx = gridSize/2.0;
for (int c=0; c<gridColumns; c++)
{
float d2 = (xx-x)*(xx-x)+(yy-y)*(yy-y);
// Naive implementation
// float d = sqrt(d2) - radius;
// if (dist[i]>d) dist[i] = d;
// Optimized implementation (no unnecessary sqrt)
float prev2 = dist[i]+radius;
prev2 *= prev2;
if (prev2 > d2)
{
float d = sqrt(d2) - radius;
if (dist[i]>d) dist[i] = d;
}
xx += gridSize;
i++;
}
yy += gridSize;
}
}
}