This is a hard task to do manually by visually placing circles along the path of a letter.
It's even harder to automatically (automagically!) to without human intervention.
Here's how to automatically arrange circles to form letters.
The answer is in 2 parts...
Finding the "letterform",
Creating circles to fill and outline the letterform.
1. The hard part
Frederik De Bleser has coded a nice library called opentype.js
that takes a .ttf font file and parses out any specified character's glyph outline using quadratic curves on a canvas: https://github.com/nodebox/opentype.js
2. The only slightly less hard part
For each letter:
Find "many" points on each quadratic curve. Here's the algorithm to calculate an [x,y] on the curve at an interval T. T will range from 0.00 at the start of the curve to 1.00 at the end of the curve. T will not produce evenly spaced [x,y]'s along the curve so you will need to oversample (So "many" might mean 1000 values of T between 0.00 and 1.00).
function getQuadraticBezierXYatT(startPt,controlPt,endPt,T) {
var x = Math.pow(1-T,2) * startPt.x + 2 * (1-T) * T * controlPt.x + Math.pow(T,2) * endPt.x;
var y = Math.pow(1-T,2) * startPt.y + 2 * (1-T) * T * controlPt.y + Math.pow(T,2) * endPt.y;
return( {x:x,y:y} );
}
Find the angle that is tangent to the curve's angle at those points. (Basically calculate what would be a right angle to the curve). You can do that with the next derivative of the quadratic formula:
function quadraticBezierTangentAngle(t, p0, p2, p1) {
var tt = 1 - t;
var dx = (tt * p1.x + t * p2.x) - (tt * p0.x + t * p1.x);
var dy = (tt * p1.y + t * p2.y) - (tt * p0.y + t * p1.y);
return Math.tan(Math.atan2(dy,dx));
}
Starting at the beginning of the curve, calculate each distance from the current [x,y] to the next [x,y]. You can do this with the Pythagorean Theorem:
var dx=nextX-currentX;
var dy=nextY-currentY;
var distance=Math.sqrt(dx*dx+dy*dy);
De-duplicate the array so that all the remaining [x,y] elements are 1px distant from the previous [x,y] element. You can do this by filling a second array with values from the first where parseInt( nextInOriginalArray - lastDistanceInNewArray)==1;
Decide on a radius for your circles that will make up each letter. This is actually harder than it might seem. For "blocky" fonts, you can draw the letter "I" on the canvas. Then fetch all pixles using getImageData
. Calculate the width of the "I"'s vertical stroke by searching for the count of opaque pixels running horizontally at the vertical middle of the letter. For blocky fonts, var radius = horizontalOpaquePixelCount/2;
. For fonts with variable width strokes, you'll have to be inventive. Maybe var radius = horizontalOpaquePixelCount/3;
or var radius = horizontalOpaquePixelCount/4;
.
Iterate through the points array and define a new circle every radius*2
pixels. You calculate the center point for each circle using the tangent angle and trigonometry like this:
var centerX = curvePointX + radius*Math.cos(tangentAngle);
var centerY = curvePointY + radius*Math.sin(tangentAngle);
While creating circles, at some point the letter's curves will turn back upon themselves, so you must check each new circle you create to be sure it won't overlap an existing circle. You can calculate whether a new circle will intersect each existing circle like this:
var dx = newCircleCenterX - existingCircleCenterX;
var dy = newCircleCenterY - existingCircleCenterY;
var distance=Math.sqrt(dx*dx+dy*dy);
var circlesAreIntersecting=(distance<=newCircleRadius+existingCircleRadius);
Fine tuning: Near some endpoints points in the letter's path, you will find that a next full radius circle will spill out of the letterform. If that occurs you could shrink the radius of some circles to fit the letterform. If you want purely a fixed radius for your circles then you can recalculate the fixed radius of all circles based on the average radii of all circles--including the ones you had to "shrink" to fit the letterform.
For example. This is the letter "L formed by 15 circles.

But the 2 red circles fall out of its letterform. You could (1) shrink the red circles to fit inside the letterform or (2) recalculate a new fixed circle radii based on the average radii that fits the letterform:
var total=0;
total += greenRadii * 13;
total += verticalRedRadiusResizedToFitInsideLetterform;
total += horizontalRedRadiusResizedToFitInsideLetterform;
var newRadius = total / 15;
You can calculate the length of the red radius that will fit the letterform by calculating the intersection of 2 lines: (1) the line segment formed by connecting the last green circles center and the red circles center, (2) the line formed perpendicularly from the last point on the curve. Here's an algorithm to calculate the intersection point of 2 lines:
// Get interseting point of 2 line segments (if any)
// Attribution: http://paulbourke.net/geometry/pointlineplane/
function line2lineIntersection(p0,p1,p2,p3) {
var unknownA = (p3.x-p2.x) * (p0.y-p2.y) - (p3.y-p2.y) * (p0.x-p2.x);
var unknownB = (p1.x-p0.x) * (p0.y-p2.y) - (p1.y-p0.y) * (p0.x-p2.x);
var denominator = (p3.y-p2.y) * (p1.x-p0.x) - (p3.x-p2.x) * (p1.y-p0.y);
// Test if Coincident
// If the denominator and numerator for the ua and ub are 0
// then the two lines are coincident.
if(unknownA==0 && unknownB==0 && denominator==0){return(null);}
// Test if Parallel
// If the denominator for the equations for ua and ub is 0
// then the two lines are parallel.
if (denominator == 0) return null;
// If the intersection of line segments is required
// then it is only necessary to test if ua and ub lie between 0 and 1.
// Whichever one lies within that range then the corresponding
// line segment contains the intersection point.
// If both lie within the range of 0 to 1 then
// the intersection point is within both line segments.
unknownA /= denominator;
unknownB /= denominator;
var isIntersecting=(unknownA>=0 && unknownA<=1 && unknownB>=0 && unknownB<=1)
if(!isIntersecting){return(null);}
return({
x: p0.x + unknownA * (p1.x-p0.x),
y: p0.y + unknownA * (p1.y-p0.y)
});
}