通过在字母路径上直观地放置圆圈来手动完成这是一项艰巨的任务。
在没有人工干预的情况下自动(自动!)变得更加困难。
这是自动排列圆圈以形成字母的方法。
答案分为两部分...
寻找“字母”,
创建圆圈以填充和勾勒字体。
1.困难的部分
Frederik De Bleser 编写了一个名为的不错的库opentype.js
,该库采用 .ttf 字体文件并使用画布上的二次曲线解析任何指定字符的字形轮廓:https ://github.com/nodebox/opentype.js
2.唯一稍微不那么硬的部分
对于每个字母:
在每条二次曲线上找到“许多”点。这是在曲线上以间隔 T 计算 [x,y] 的算法。T 的范围从曲线开始处的 0.00 到曲线结束处的 1.00。T 不会沿曲线产生均匀间隔的 [x,y],因此您需要过采样(因此“许多”可能意味着 1000 个 T 值在 0.00 和 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} );
}
找到在这些点处与曲线角度相切的角度。(基本上计算与曲线成直角的角度)。您可以使用二次公式的下一个导数来做到这一点:
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));
}
从曲线的开头开始,计算从当前 [x,y] 到下一个 [x,y] 的每个距离。你可以用勾股定理做到这一点:
var dx=nextX-currentX;
var dy=nextY-currentY;
var distance=Math.sqrt(dx*dx+dy*dy);
对数组进行重复数据删除,以使所有剩余的 [x,y] 元素与前一个 [x,y] 元素的距离为 1px。您可以通过使用第一个数组中的值填充第二个数组来做到这一点parseInt( nextInOriginalArray - lastDistanceInNewArray)==1;
确定构成每个字母的圆圈的半径。这实际上比看起来更难。对于“块状”字体,您可以在画布上绘制字母“I”。然后使用getImageData
. 通过搜索在字母垂直中间水平运行的不透明像素的计数来计算“I”的垂直笔划的宽度。对于块状字体,var radius = horizontalOpaquePixelCount/2;
. 对于具有可变宽度笔画的字体,您必须具有创造性。也许var radius = horizontalOpaquePixelCount/3;
或var radius = horizontalOpaquePixelCount/4;
。
遍历点数组并为每个radius*2
像素定义一个新圆。您可以使用切角和三角函数计算每个圆的中心点,如下所示:
var centerX = curvePointX + radius*Math.cos(tangentAngle);
var centerY = curvePointY + radius*Math.sin(tangentAngle);
在创建圆圈时,字母的曲线会在某些时候自动返回,因此您必须检查您创建的每个新圆圈,以确保它不会与现有圆圈重叠。您可以计算一个新圆是否会与每个现有圆相交,如下所示:
var dx = newCircleCenterX - existingCircleCenterX;
var dy = newCircleCenterY - existingCircleCenterY;
var distance=Math.sqrt(dx*dx+dy*dy);
var circlesAreIntersecting=(distance<=newCircleRadius+existingCircleRadius);
微调:在字母路径中的一些端点附近,您会发现下一个完整半径的圆会溢出字母形状。如果发生这种情况,您可以缩小一些圆圈的半径以适应字体。如果您只想要一个固定半径的圆圈,那么您可以根据所有圆圈的平均半径重新计算所有圆圈的固定半径 - 包括您必须“缩小”以适应字体的那些。
例如。这是由 15 个圆圈组成的字母“L”。
但是 2 个红色圆圈从它的字体中掉了出来。您可以 (1) 缩小红色圆圈以适应字形或 (2) 根据适合字形的平均半径重新计算新的固定圆半径:
var total=0;
total += greenRadii * 13;
total += verticalRedRadiusResizedToFitInsideLetterform;
total += horizontalRedRadiusResizedToFitInsideLetterform;
var newRadius = total / 15;
您可以通过计算 2 条线的交点来计算适合字母形状的红色半径的长度:(1)通过连接最后一个绿色圆圈中心和红色圆圈中心形成的线段,(2)垂直形成的线曲线上的最后一点。这是计算两条线的交点的算法:
// 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)
});
}