前言
在最近的生成艺术项目中遇到一个小问题:如何在作品中优雅地添加文本元素,同时避免使用网络字体,要么侵权要么花钱~~给项目增加不必要的负担,我决定不走寻常路,自己动手,丰衣足食,用JS打造一款自己的专用字体!
问题分析
首先,虽然网上字体库很多而且也比较方便,但除了某些字体版权限制之外其显示效果往往受限于用户的浏览器和设置,导致在不同环境下出现视觉差异,这对于追求视觉一致性的艺术作品来说是不可接受的。其次,直接嵌入字体文件虽然能解决一致性问题,但会增加页面加载时间,影响用户体验。更关键的是,我渴望在这个项目中实现每一个视觉元素的自主创作,让作品真正打上“原创”的标签。
解决方案:手写字母表
于是,我萌生了一个大胆的想法:使用p5.js这个强大的图形库结合JavaScript的灵活编程能力,自行绘制一套路径式字母表。这种方法的好处在于,我可以完全控制每个字母的形状、线条粗细、颜色等属性,实现高度的自定义和个性化。
实现步骤
1. 实现原理
在深入探索字体设计的奥秘时,首先系统地查阅了字体结构中那些关键点的术语与位置,它们如同构建字体的基石,为我后续的创作提供了坚实的理论基础。然而,在实际动中生成手绘制字母时,并没有遵循传统路径,而是选择了一条更为直观且富有创意的道路——以中心点(mid_x, mid_y)观察字母间的间距调整(kerning)时,意识到如果从字母底部左侧这一自然起点出发进行定义,将能更有效地解决当前存在的一些细微不一致问题。于是,在Letter类的设计中,重新布局了包括x高度、顶端高度在内的多个关键位置参数,这些参数不仅与字体大小紧密相关,更以中心点为基准,构建了一个灵活而精确的坐标体系。
为了进一步提升字母的精细度和美观度,还引入了一系列与高度成比例的小距离调整值。这些调整值,如this.adj_1
至this.adj_4
等,通过乘以字体全高(h_full)的不同比例系数得出,为字母的每一个细节点提供了微调的可能性。例如,this.adj_1
是字体全高的5%,而this.adj_4
则是其20%,这样的设计使得在保持整体协调的同时,能够轻松地对字母的局部进行精细雕琢。如下是设定的一些与高度相关的小距离,用来微调各个点的位置。具体如下:
this.adj_1 = this.h_full * 0.05;
this.adj_15 = this.h_full * 0.075;
this.adj_2 = this.h_full * 0.1;
this.adj_25 = this.h_full * 0.125;
this.adj_3 = this.h_full * 0.15;
this.adj_35 = this.h_full * 0.175;
this.adj_4 = this.h_full * 0.2;
2. 定义字母
在纸上随手写下每个字母,捕捉其自然的流动与形态。这些草图是我对字母最直观的反映。再利用p5.js,根据草图勾勒出字母的初步路径。这些路径由上述关键点组成,它们标记了字母轮廓的转折点。路径的初步定义往往不够完美,需要反复调整。通过移动关键点的位置、修改路径的曲率,甚至添加新的点来细化字母的形状,以确保字母既符合审美又易于识别。字母的形状达到满意后,我会进行最终的测试,确保它在不同尺寸和背景下都能保持良好的显示效果,
create_a(){
this.paths = [
[ // stem
{x: this.x_left+this.adj_2, y: this.y_x + this.adj_4},
{x: this.x_left+this.adj_3, y: this.y_x + this.adj_1},
{x: this.x_mid+this.adj_2, y: this.y_x},
{x: this.x_right, y: this.y_x+this.adj_2},
{x: this.x_right, y: this.y_base-this.adj_4},
{x: this.x_right+this.adj_1, y: this.y_base},
],
[ // round
{x: this.x_right-this.adj_1, y: this.y_mid-this.adj_15},
{x: this.x_mid-this.adj_1, y: this.y_mid-this.adj_1},
{x: this.x_left, y: this.y_mid+this.adj_35},
{x: this.x_mid, y: this.y_base},
{x: this.x_mid+this.adj_2, y: this.y_base-this.adj_1},
{x: this.x_right+this.adj_2, y: this.y_base - this.adj_4},
]
];
}
3. 路径弯曲处理
使用 Chaikin 曲线算法来平滑路径
Chaikin 平滑曲线算法是一种递归方法,用于对多边形(或路径)进行平滑处理,使得其形状更加自然和流畅。这种方法特别适用于计算机图形学和字体设计等领域,因为它能够生成视觉上更加吸引人的曲线。
这个图可以直观的看出来它是如何工作的
接下来我将详细解释如何使用 Chaikin 算法来平滑一条简单的路径。假设我们有一个由点集 P = {p0, p1, ..., pn}
组成的路径,其中 n
是点的总数。
Chaikin 平滑算法步骤
-
初始化:首先,我们有一个由
n+1
个点(包括起始点和终止点)组成的路径P
。 -
递归步骤:在每一轮递归中,我们创建一个新的点集
P'
,它包含以下点:- 开始点:
P'
的第一个点是P
的第一个点p0
。 - 中间点:对于
P
中的每一对相邻点(pi, pi+1)
(其中i
从0
到n-2
),我们在距离pi
25% 和距离pi+1
75% 的位置插入两个新点。这两个新点的位置可以用向量计算得出,例如,第一个新点qi
可以用(3/4) * pi + (1/4) * (pi+1)
计算,第二个新点qi+1
用(1/4) * pi + (3/4) * (pi+1)
计算。 - 结束点:
P'
的最后一个点是P
的最后一个点pn
。
- 开始点:
-
递归终止:重复上述步骤,直到路径
P'
足够平滑或达到预设的递归深度。
我们按照以下步骤创建一条新路径:
-
复制第一个点(末端保持原位)
-
对于最后一点之前的其余点:
-
在距上一点 25% 的位置添加一个点
-
在距离下一个点 25% 的位置添加一个点
-
-
复制最后一点。
一轮之后,我们得到了这个。新路径用红色标记。
然后我们对得到的路径应用相同的步骤。得到下面第 2 轮和第 3 轮之后的结果。
最终结果如下
经过一轮处理后,路径变得更加平滑。对于小尺寸字体,3 到 4 轮的 Chaikin 算法迭代就足够了。如果字体尺寸较大,可以增加迭代次数,确保边缘不过于尖锐。
代码优化
通过将路径定义与中点、字母顶点等关键位置关联起来,可以更清晰地理解如何绘制字母路径,例如,与其说“b 的主干从中点上方 14.1 像素处开始,到中点下方 7.4 像素处结束”,不如简单地理解为“b 的主干从字母顶点延伸到字母底部”,这样更容易理解。
然而,最终生成的代码非常冗长,不够简洁,就像上面的 create_a
函数那样,看上去就乱
为了解决这个问题,编写了一个函数用来遍历每个字母并生成新的代码将所有内容转换成简单的数值格式,如下:
let string = "";
for(let l of this.letters){
string += "create_" + l.letter + "(){\n";
string += " this.ip = [\n";
for(let path of l.ip){
string += " [";
for(let p of path){
string += "{x: " + nf(p.x, 0, 1) + ", y: " + nf(p.y, 0, 1) + "}";
if (path.indexOf(p) != path.length-1) string += ", ";
}
string += "]";
if (l.ip.indexOf(path) != l.ip.length-1) string += ",\n";
else string += "\n";
}
string += " ]\n";
string += "}\n";
}
console.log(string)
这样一来,生成字母啊的函数大大减少了:
create_a(){
this.ip = [
[{x: -2.8, y: -3.4}, {x: -1.7, y: -6.8}, {x: 2.3, y: -8.0}, {x: 5.4, y: -5.7}, {x: 5.4, y: 2.9}, {x: 6.6, y: 7.4}],
[{x: 4.3, y: -1.7}, {x: -0.9, y: -1.1}, {x: -5.1, y: 3.9}, {x: -2.1, y: 7.4}, {x: 2.3, y: 6.3}, {x: 5.4, y: 2.9}]
]
}
重塑字母路径宽度
这是目前运行之后生成的所有字母
看起来挺自然,不过我感觉线条粗细过于细长且单调,毕竟手写出来的字体,不可能有这么规范,为了更加贴近真正的手写字母实现不同宽度的路径,我使用了另一种算法函数将路径变成了二维形状,还是最初这张图,稍微讲解一下是如何实现路径宽度变化的
点成线,线成面,要想实现一个二维路径,肯定不能只有单一的点组合,为了创建形状路径,我们沿着这条路径行进,在每个点处:
-
找到从该点到下一个点的角度(对于最后一个点,找到到前一个点的角度并将其翻转 180°)
-
使用 Perlin 噪声,选择该点的路径宽度。
这里我在路径的每个点都画了一条线,以展示这些角度和宽度。请注意每条线的长度略有不同,并且它们的角度是跟随路径曲线不断变化的
这样就可以绘制路径的内外边缘,形成一个具有宽度变化的路径
很容易看出如何在内部路径周围绘制一条路径来创建宽度变化的路径。在线条的末端,绘制了一个围绕 180° 的小点环,以创建一个完整的圆形结尾。
不过这个算法也并不完美,当笔触宽度较宽时,这些尴尬由于颜色相同也压根看不出什么问题,所以这点小BUG貌似并不重要。
最最最最后,为了让结果看起来更加自然,我还使用 Perlin 噪声稍微抖动所有点,这为字母增加了另一层自然感,让他们更加随机多样
整个字母表的样子如下:
经过这些步骤,最终生成了一套完整的手写字母,尽管整个过程非常耗时,但最终的效果其实和手写效果基本一致,字母线条的厚度自然变化,使得视觉效果更为生动,当然你也可以根据字母的位置来改变线宽。