当前位置: 首页 > news >正文

使用程序绘制中文字体——中文字体的参数化设计方案初探

目录

      • 写在前面
      • 基本设计思路
      • 笔画骨架参数设计
      • 笔画风格参数设计
        • 起笔风格
        • 转角风格
        • 字重变化
        • 弯曲程度
      • 字形的“组装拟合”
      • 基于骨架的结构调整
      • 笔画绘制二三事
        • 撇的两侧轮廓绘制——不是两条贝塞尔曲线那么简单
        • 转角的处理,怎样能显得不突兀?
        • 笔画骨架关键点的拖拽编辑
      • 未来展望

写在前面

笔者一直非常羡慕写字画画很好的朋友,但是自己小时候没有培养起来这方面的特长,长大后接触编程,就“异想天开”能不能使用程序帮助自己写字画画。上学的时候,笔者在使用程序“参数化绘画”方向做了很多“闭门造车”式的尝试,最后效果都不太理想。步入社会后一直没有忘记当初的构想,但是使用程序绘画,是个极其复杂的工程,里面涉及参数过多,很难精简,在用户体验和图画效果的权衡上很难做取舍。偶然接触字体和图标设计领域,由最初的好奇到对行业前沿有一定的了解,发现这个领域的一些前沿应用与自己的构想不谋而合,比如可变字体标准的推出等等。诚然,相比与图画,字体笔画的设计更加精简,数字化更加流行,尤其对中文字体,在参数化、数字化字形方向还有很大的研究空间。于是笔者就将自己的精力专注于中文字体设计的参数化设计方向,基于这个初心,做了一款目前尚且简陋的字体设计工具——《字玩》。

字玩开源地址:gitee | github
字玩介绍:字玩官网

使用自己制作的工具字玩,笔者初步尝试了实现程序绘制笔画,再制作可视化的界面让用户可以将笔画“组装”成字形,并可以通过拖拽笔画调整骨架:
请添加图片描述

上图中绘制的“永”字笔画设计参考的黑体,之所以选择黑体作为入门,是因为黑体笔画构架相对简单,适合使用程序绘制并做参数化设计。其实,笔者最初的尝试是使用绘制“隶书”风格的笔画,比如,笔者实用程序的方式绘制了隶书“蚕头燕尾”的横:
请添加图片描述

但是,隶书笔画没有黑体那样规整,就造成了很多问题:一是参数过多且不易理解,比如隶书蚕头的绘制就囊括了左切角、上切角、下切角等多个参数,二是在动态调整期间,绘制效果不尽如人意,比如在长度为500时蚕头燕尾还是正常的,但是缩短长度到100就没有办法看了。

思来想去,笔者最终决定暂时搁置隶书的程序化进程,尝试使用黑体作为入门。通过一段时间的努力,笔者使用程序绘制了黑体常用的32个笔画,并基于“笔画组装字形”的设想,参考思源黑体的结构,制作了《登鹳雀楼》一诗中包含的20个中文字形:
请添加图片描述

在上述图片中,20个中文字形的笔画全部由程序进行绘制,在字玩中通过调参将笔画组合成字形,效果基本可以达到预期。这篇博客就简单介绍一下在程序化、参数化绘制黑体字形时,作出的设计和遇到的问题。

基本设计思路

笔者将探索过程中的设计思路抽象成简单两点:

  1. 笔画->字形:由笔画经过调参,组合成字形
  2. 骨架+风格->笔画:对于单个笔画的绘制,抽象出骨架参数和风格参数。骨架参数由尽量精简的参数确定笔画骨架,在确定骨架的基础上,由风格参数控制笔画风格的调整

字形,顾名思义,即字体呈现出的最终轮廓形状。相比于用钢笔工具一笔笔勾勒草图上的轮廓数据,越来越多的设计师选择用“部件”的方式,将笔画作为最小单元,“组合拼装”成字形。笔者采纳了这种设计方式:通过复用仅仅32个常用笔画,就可以拼装成7000个不同的汉字,这将极大的提高设计效率。

确定了由笔画组装成字形的设计思路,如何设计笔画的参数就变得至关重要。相比于“好看”,我们的首要目的是通过调参的方式,可以由32个常用笔画“拟合”出任意汉字,并且让参数尽可能精简、易懂。其次,才是如何将笔画绘制得好看。基于这一点,笔者将参数分为两类:第一类参数是骨架参数,用于绘制笔画骨架、结构,这类参数要尽量精简,易懂。第二类参数为风格参数,用于绘制笔画的样式、风格,这类参数可以不断扩展,使用同一个骨架,可以绘制无数个不同的风格。这样,就抽象出两层参数组:骨架参数和风格参数。打个比方,黑体、宋体、楷体三个风格的字体,完全可以使用同一组骨架,仅仅改变风格参数,就可以使同样结构的字体变为不同的风格。

笔画骨架参数设计

确定了基本设计思路,接下来就是最重要也是最基本的环节:设计笔画骨架以及其参数。

听到“骨架”二字,读者或许会有疑惑所谓“骨架”到底是什么?是一组贝塞尔曲线?还是一组线段?这些好像都可以作为骨架。但是字玩中,笔者选择将骨架数据由一组关键点表示。只要给出关键点,程序就可以根据这组关键点生成相应笔画的基础轮廓。

打个简单的比方,笔画“横”的骨架数据为:

const skeleton = {start: { x, y },end: { x, y },
}

骨架数据给出了横之骨架的起点和终点,接下来在绘制的时候,程序就会根据起点和终点绘制横的轮廓,比如:

pen.moveTo(skeleton.start.x, skeleton.start.y)
pen.lineTo(skeleton.end.y, skeleton.end.y)

当然,上述代码只是做一个简单的示例,最终的绘制代码肯定比上述代码要复杂很多。

这时候读者可能又会疑惑:那么,骨架的关键点要怎么生成呢?难道需要让用户手动点击屏幕设置关键点么?尽管字玩确实实现了拖拽改变关键点位置的功能,但是直观上关键点肯定不是直接面向用户的参数。直接面向用户的,也就是需要让用户设置的,是更直观的基本参数。比如对于笔画“横”,抽象出来的骨架参数只有“长度”一个,只要有了“长度”这个参数,程序会自动计算相应关键点。读者可能会问,参数中没有实际坐标,怎么能确定关键点呢?事实上,在默认情况下,笔画会被画在屏幕中央,当用户引用某个笔画组件时,可以以拖拽或输入的方式手动改变笔画的位置(ox, oy),这时候关键点位置会被重新计算,笔画也会随之移动。

综上所述,我们目前的任务是设计绘制每个笔画所需的骨架关键点,以及通过这些关键点抽象出来更直观的基本参数供用户调整。

在进行关键点和基本参数设计之前,我们先小小研究一下笔画特征:虽然笔者整理出的常用笔画总共有32个,但是这些笔画中,基础笔画仅有“横竖撇捺折点挑”七个,其他大多都是由基础笔画拼成的“复合笔画”。比如下图中的“横折钩”笔画,就可以拆解成“横”、“折”、“钩”三个基础笔画,而其中“折”和“钩”两个笔画其实都可以算作笔画“折”。所谓“折”的骨架,就是可以旋转任意角度而没有弯曲的线段,与“横”、“竖”的区别仅仅是多了角度的旋转。以此类推,全部32个笔画都可以使用“横竖撇捺折点挑”这七个笔画组成,所以我们的首要任务就是设计这7个基础笔画的参数。

问题到了现在变得简单多了。

首先,对于“横”和“竖”的骨架设计比较直观,不考虑角度问题,也就是默认只能“横平竖直”的情况下,“横”、“竖”的骨架关键点可以由两个端点构成。而在知道笔画组件位置(ox, oy)的情况下,可以提取出“长度”这个比较直观的参数。

对于“折”,也就是可以旋转任意角度的线段,关键点也可以由两个端点确定。至于面向用户的基本参数提取上,可能最直观的是使用“长度”+“角度”的方式。但是笔者在字玩中没有采用这种设计,而是将参数设计为“水平延伸”和“竖直延伸”两个。所谓水平延伸,即两个端点间的水平距离,而竖直延伸,则为两个端点的竖直距离。水平延伸和竖直延伸都可以设置为负数,通过正负区分折的方向。

为什么采用这种设计呢?主要是为了比较进阶的功能“参数与布局绑定“而考量的——在字玩中,用户可以对组件设置布局,并将笔画参数与布局参数进行绑定,以达到改变布局便能改变笔画骨架的效果。打个最常见的比方,对于部首“木字旁”,在不同的汉字中,占宽可能不尽相同,比如“林”和“树”两个字,尽管都要使用“木字旁”,但是“树”中的“木字旁”明显要更窄一些。这时候,在复用组件时,我们可以为“木字旁”添加一个最简单的矩形布局,由“长”“宽”两个参数确定布局。为了实现改变布局(比如调整“木”的宽度)时笔画被重新绘制以适应新布局,我们可以将布局参数与笔画骨架参数进行绑定。这时候,如果使用“长度”+“角度”的参数设计,很难直观上与宽高对应——如果将笔画“撇”的“长度”与部首组件“木”的“宽度”参数绑定,在改变宽度时,很可能会出现撇长度过长或错位的效果,而我们仅仅希望在“木字旁”变宽一些的时候,撇可以在不被压缩字重的情况下,水平延伸更大一些以占满布局。所以对于笔画“折”,笔者最终采用“水平延伸”和“竖直延伸”两个参数以确定关键点。
请添加图片描述

对于“撇”、“捺”、“点”、“挑”四个笔画,与“折”的区别就是在角度的基础上,增加了弯曲度。所以笔者沿用“水平延伸”和“竖直延伸”两个参数来确定“撇”、“捺”、“点”、“挑”的起点与终点,并在此基础上,增加一个拐点来确定弯曲度。“撇”、“捺”、“点”、“挑”的骨架关键点相同,都是由起点、终点、拐点三个关键点构成的集合,而最终的笔画骨架成像,就是以两个端点为锚点,并以拐点为控制点的二次贝塞尔曲线。为了确定拐点的位置,笔者增加了两个面向用户的参数:弯曲度和弯曲游标。弯曲度确定了拐点到两个端点连线的垂直距离;而弯曲游标确定了拐点到两个端点作垂线的垂足位置。
请添加图片描述

最终,全部笔画生成如下:
请添加图片描述

笔画风格参数设计

有了笔画骨架,也就可以生成笔画的基本轮廓,但是这样笔画毕竟略显单调,要是希望笔画变得再好看一些就需要进行风格参数的设计。除了字重之外,目前字玩仅仅做了用于测试的四种风格参数支持,设计上还比较简单,仅供参考:

起笔风格

目前支持三种起笔风格:默认样式,凸笔起笔,凸笔圆角起笔,样式分别为下图所示。
请添加图片描述
对于每种起笔样式,还增设了一个“起笔数值”的数值参数控制对应风格的细节调整。比如对于凸笔圆角起笔,在改变“起笔数值”参数的情况下,样式会对应发生改变。

绘制起笔的代码示例:

if (start_style_type === 0) {// 无起笔样式pen.moveTo(out_heng_start.x, out_heng_start.y)
} else if (start_style_type === 1) {// 起笔上下凸起长方形pen.moveTo(out_heng_start.x, out_heng_start.y - start_style.start_style_decorator_height)pen.lineTo(out_heng_start.x + start_style.start_style_decorator_width, out_heng_start.y - start_style.start_style_decorator_height)pen.lineTo(out_heng_start.x + start_style.start_style_decorator_width, out_heng_start.y)
} else if (start_style_type === 2) {// 起笔上下凸起长方形,长方形内侧转角为圆角pen.moveTo(out_heng_start.x, out_heng_start.y - start_style.start_style_decorator_height)pen.lineTo(out_heng_start.x + start_style.start_style_decorator_width, out_heng_start.y - start_style.start_style_decorator_height)pen.quadraticBezierTo(out_heng_start.x + start_style.start_style_decorator_width,out_heng_start.y,out_heng_start.x + start_style.start_style_decorator_width + start_style.start_style_decorator_radius,out_heng_start.y,)
}
转角风格

目前支持两种转角风格:默认样式,转角圆滑凸起,样式分别为下图所示。
请添加图片描述

对于每种起笔样式,还增设了一个“转角数值”的数值参数控制对应风格的细节调整。比如对于转角圆滑凸起,在改变“转角数值”参数的情况下,样式会对应发生改变。

绘制转角的代码示例:

if (bending_degree > 1 && turn_style_type === 0) {// 绘制外侧横折圆角pen.lineTo(out_radius_start_heng_zhe.x, out_radius_start_heng_zhe.y)pen.quadraticBezierTo(out_corner_heng_zhe.x, out_corner_heng_zhe.y, out_radius_end_heng_zhe.x, out_radius_end_heng_zhe.y)
} else if (turn_style_type === 0) {pen.lineTo(out_corner_heng_zhe_up.x, out_corner_heng_zhe_up.y)pen.lineTo(out_corner_heng_zhe_down.x, out_corner_heng_zhe_down.y)
} else if (turn_style_type === 1) {// 转角样式1pen.lineTo(turn_data.turn_start_1.x, turn_data.turn_start_1.y)pen.quadraticBezierTo(turn_data.turn_control_1.x, turn_data.turn_control_1.y, turn_data.turn_end_1.x, turn_data.turn_end_1.y)pen.lineTo(turn_data.turn_end_2.x, turn_data.turn_end_2.y)pen.quadraticBezierTo(turn_data.turn_control_2.x, turn_data.turn_control_2.y, turn_data.turn_start_2.x, turn_data.turn_start_2.y)
}
字重变化

字重变化为单个数值参数,表示笔画字重由起笔到收笔的变化程度,目前仅支持改变撇、捺的字重变化。效果如下:
请添加图片描述

弯曲程度

弯曲程度为单个数值参数,可以影响撇和捺弯曲程度,同时在默认转角样式下,可以影响转角弯曲程度。效果如下:
请添加图片描述

字形的“组装拟合”

设计好笔画参数,字形的“组装拟合”看起来是个简单的步骤,但这其中还是会遇到不少细节问题。

我们首先来看一下一个简单的“口”字的组装:

组装时,为了使笔画连接处闭合,我们很直观地将“竖”和“横折”贴靠在了一起,但是当改变字重时,我们会发现,笔画错位了:
请添加图片描述

这是由于什么造成的呢?原来在默认情况下,“竖”的骨架关键点处于笔画两侧轮廓的正中央,在改变字重时,“竖”增宽了,这会导致“竖”最左侧的轮廓向左移动,而“横折”的起笔在字重变化时没有发生改变,就导致了错位现象。

为了解决这个问题,笔者增加了一个参数“参考位置”,指代笔画骨架的固定参考位置为右侧(上侧)、左侧(下侧)还是默认的处于中间。我们将“口”字的笔画“竖”之参考位置改为左侧(下侧),也就是骨架固定在笔画最左侧,当改变字重时,左侧轮廓位置是不变的。同时,将“横折”的参考位置改变为右侧(上侧)。再次改变字重,错位问题就解决了:
请添加图片描述

解决了改变字重之后的错位问题,其实还有一个重要问题没有解决——自动吸附对齐。我们在设计时,经常需要让笔画连接处闭合对齐,这在成熟设计工具中可以使用吸附对齐的方式帮助用户快速调节。但是目前在字玩中,还没有做这方面的工作,需要用户手动对齐笔画,其实是比较麻烦的。这也作为未来的一项待办任务。

基于骨架的结构调整

在已知字体轮廓的情况下,我们可以使用程序对其做出结构上的调整,比较常见的调整方式为调整中宫和重心。中宫,即将字形绘制在九宫格中时,中心格所占的大小。调整中心格的大小可以改变字形的紧收程度。而重心则表示字形“重量感”的集中点。

字玩中支持用户使用直观拖拽九宫格的方式调整任意字形轮廓的中宫与重心。但是,在默认情况下,调整中宫使中宫变大时,会使越靠近中心的轮廓字重变得宽厚,靠近边缘的轮廓字重变得窄小:
请添加图片描述

这是由于改变中宫后,重新计算各个轮廓点的新坐标时,中心格被放大,其中的点阵也按比例扩大而造成的。

为了解决这一问题,对于使用了骨架作为基本数据的笔画,字玩支持“基于骨架的调整”方式:在调整中宫时,仅重新计算骨架关键点的位置,然后根据新的骨架关键点生成字形。效果如下:
请添加图片描述

由于骨架是没有字重的连线,所以改变骨架关键点不会使轮廓“扩张”。这样,问题就得到圆满解决。

笔画绘制二三事

讲到现在,读者可能会觉得烦躁:说了这么多概念,到底绘制笔画的程序代码在哪?这一节我们将以笔画“横”的脚本代码作为示例,仔细聊聊如何使用程序绘制笔画,以及其中遇到的问题和解决方案。

需要绘制笔画,首先我们需要读取参数,在字玩中,我们可以使用API glyph.getParam('paramName') 来读取指定参数:

const params = {length: glyph.getParam('长度'),skeletonRefPos: glyph.getParam('参考位置'),
}
const global_params = {weights_variation_power: glyph.getParam('字重变化'),start_style_type: glyph.getParam('起笔风格'),start_style_value: glyph.getParam('起笔数值'),turn_style_type: glyph.getParam('转角风格'),turn_style_value: glyph.getParam('转角数值'),bending_degree: glyph.getParam('弯曲程度'),weight: glyph.getParam('字重') || 40,
}

接下来,我们需要根据这些参数生成用于绘制形状的关键点。最基本的关键点自然是骨架关键点:

const { length, skeletonRefPos } = params
const { weight } = global_paramslet start, end
const start_ref = new FP.Joint('start_ref',{x: x0,y: y0,},
)
const end_ref = new FP.Joint('end_ref',{x: start_ref.x + length,y: start_ref.y,},
)
if (skeletonRefPos === 1) {// 骨架参考位置为右侧(上侧)start = new FP.Joint('start',{x: start_ref.x,y: start_ref.y + weight / 2,},)end = new FP.Joint('end',{x: end_ref.x,y: end_ref.y + weight / 2,},)
} else if (skeletonRefPos === 2) {// 骨架参考位置为左侧(下侧)start = new FP.Joint('start',{x: start_ref.x,y: start_ref.y - weight / 2,},)end = new FP.Joint('end',{x: end_ref.x,y: end_ref.y - weight / 2,},)
} else {// 默认骨架参考位置,即骨架参考位置为中间实际绘制的骨架位置start = new FP.Joint('start',{x: start_ref.x,y: start_ref.y,},)end = new FP.Joint('end',{x: end_ref.x,y: end_ref.y,},)
}
glyph.addJoint(start_ref)
glyph.addJoint(end_ref)
glyph.addRefLine(refline(start_ref, end_ref, 'ref'))glyph.addJoint(start)
glyph.addJoint(end)const skeleton = {start,end,
}glyph.addRefLine(refline(start, end))

当看到如下代码:

const skeleton = {start,end,
}

我们可以很清晰地看出,笔画“横”的骨架关键点仅包含两个端点:起点和终点。但是有些读者可能会觉得上述代码有些复杂,两个端点的坐标计算在不同情况下是不一样的,这是为什么呢?在“由笔画组装字形”一节中,我们已经了解到“参考位置”的概念,也就是当用户改变字重时,骨架固定在笔画中间,还是固定在上侧或下侧。允许用户手动设置“参考位置”,可以有效解决调整字重时笔画连接处错位的问题。所以,我们要先生成参考骨架start_refend_ref,然后根据参考位置的不同,计算最终用于生成笔画轮廓的“中间骨架”。

有了骨架数据,我们就可以基于骨架绘制笔画了。首先,我们要根据骨架计算绘制形状所需的其他关键点:

// 根据骨架计算轮廓关键点
const { start, end } = skeleton// out指上侧(外侧)轮廓线
// in指下侧(内侧)轮廓线
const { out_heng_start, out_heng_end, in_heng_start, in_heng_end } = FP.getLineContours('heng', { heng_start: start, heng_end: end }, weight)

可以看到,我们计算出了四个关键点:out_heng_start, out_heng_end, in_heng_start, in_heng_end,它们分别为笔画横的上侧轮廓起始点和终点,以及下侧轮廓起始点和终点。

有了这些关键点,我们就可以开始绘制笔画形状了。对于笔画“横”,我们可以调节笔画起始风格,所以,针对不同风格,绘制代码会有一些小小的不同:

// 创建钢笔组件
const pen = new FP.PenComponent()
pen.beginPath()// 绘制横的上侧轮廓
if (start_style_type === 0) {// 无起笔样式pen.moveTo(out_heng_start.x, out_heng_start.y)
} else if (start_style_type === 1) {// 起笔上下凸起长方形pen.moveTo(out_heng_start.x, out_heng_start.y - start_style.start_style_decorator_height)pen.lineTo(out_heng_start.x + start_style.start_style_decorator_width, out_heng_start.y - start_style.start_style_decorator_height)pen.lineTo(out_heng_start.x + start_style.start_style_decorator_width, out_heng_start.y)
} else if (start_style_type === 2) {// 起笔上下凸起长方形,长方形内侧转角为圆角pen.moveTo(out_heng_start.x, out_heng_start.y - start_style.start_style_decorator_height)pen.lineTo(out_heng_start.x + start_style.start_style_decorator_width, out_heng_start.y - start_style.start_style_decorator_height)pen.quadraticBezierTo(out_heng_start.x + start_style.start_style_decorator_width,out_heng_start.y,out_heng_start.x + start_style.start_style_decorator_width + start_style.start_style_decorator_radius,out_heng_start.y,)
}
pen.lineTo(out_heng_end.x, out_heng_end.y)// 绘制轮廓连接线
pen.lineTo(in_heng_end.x, in_heng_end.y)// 绘制横的下侧轮廓
if (start_style_type === 0) {// 无起笔样式pen.lineTo(in_heng_start.x, in_heng_start.y)
} else if (start_style_type === 1) {// 起笔上下凸起长方形pen.lineTo(in_heng_start.x + start_style.start_style_decorator_width, in_heng_start.y)pen.lineTo(in_heng_start.x + start_style.start_style_decorator_width, in_heng_start.y + start_style.start_style_decorator_height)pen.lineTo(in_heng_start.x, in_heng_start.y + start_style.start_style_decorator_height)
} else if (start_style_type === 2) {// 起笔上下凸起长方形,长方形内侧转角为圆角pen.lineTo(in_heng_start.x + start_style.start_style_decorator_width + start_style.start_style_decorator_radius,in_heng_start.y,)pen.quadraticBezierTo(in_heng_start.x + start_style.start_style_decorator_width,in_heng_start.y,in_heng_start.x + start_style.start_style_decorator_width,in_heng_start.y + start_style.start_style_decorator_height,)pen.lineTo(in_heng_start.x, in_heng_start.y + start_style.start_style_decorator_height)
}// 绘制轮廓连接线
if (start_style_type === 0) {// 无起笔样式pen.lineTo(out_heng_start.x, out_heng_start.y)
} else if (start_style_type === 1) {// 起笔上下凸起长方形pen.lineTo(out_heng_start.x, out_heng_start.y - start_style.start_style_decorator_height)
} else if (start_style_type === 2) {// 起笔上下凸起长方形,长方形内侧转角为圆角pen.lineTo(out_heng_start.x, out_heng_start.y - start_style.start_style_decorator_height)
}pen.closePath()

代码中,我们使用了字玩内置API FP.PenComponent 创建了一个钢笔组件,并根据起始风格的不同,绘制出了不同风格的笔画横。

为了讲解方便,笔者省略了脚本中的一些辅助代码,笔画“横”的完整代码如下:

const ox = 500
const oy = 500
const x0 = 250
const y0 = 500
const params = {length: glyph.getParam('长度'),skeletonRefPos: glyph.getParam('参考位置'),
}
const global_params = {weights_variation_power: glyph.getParam('字重变化'),start_style_type: glyph.getParam('起笔风格'),start_style_value: glyph.getParam('起笔数值'),turn_style_type: glyph.getParam('转角风格'),turn_style_value: glyph.getParam('转角数值'),bending_degree: glyph.getParam('弯曲程度'),weight: glyph.getParam('字重') || 40,
}const getJointsMap = (data) => {const { draggingJoint, deltaX, deltaY } = dataconst jointsMap = Object.assign({}, glyph.tempData)switch (draggingJoint.name) {case 'end': {jointsMap['end'] = {x: glyph.tempData['end'].x + deltaX,y: glyph.tempData['end'].y,}break}}return jointsMap
}glyph.onSkeletonDragStart = (data) => {// joint数据格式:{x, y, name}const { draggingJoint } = dataglyph.tempData = {}glyph.getJoints().map((joint) => {const _joint = {name: joint.name,x: joint.x,y: joint.y,}glyph.tempData[_joint.name] = _joint})
}glyph.onSkeletonDrag = (data) => {if (!glyph.tempData) returnglyph.clear()// joint数据格式:{x, y, name}const jointsMap = getJointsMap(data)const _params = computeParamsByJoints(jointsMap)updateGlyphByParams(_params, global_params)
}glyph.onSkeletonDragEnd = (data) => {if (!glyph.tempData) returnglyph.clear()// joint数据格式:{x, y, name}const jointsMap = getJointsMap(data)const _params = computeParamsByJoints(jointsMap)updateGlyphByParams(_params, global_params)glyph.setParam('长度', _params.length)glyph.tempData = null
}const range = (value, range) => {if (value < range.min) {return range.min} else if (value > range.max) {return range.max}return value
}const computeParamsByJoints = (jointsMap) => {const { start, end } = jointsMapconst length_range = glyph.getParamRange('长度')const length = range(end.x - start.x, length_range)return {length,skeletonRefPos: glyph.getParam('参考位置'),}
}const refline = (p1, p2, type) => {const refline =  {name: `${p1.name}-${p2.name}`,start: p1.name,end: p2.name,}if (type) {refline.type = type}return refline
}const updateGlyphByParams = (params, global_params) => {const { length, skeletonRefPos } = paramsconst { weight } = global_paramslet start, endconst start_ref = new FP.Joint('start_ref',{x: x0,y: y0,},)const end_ref = new FP.Joint('end_ref',{x: start_ref.x + length,y: start_ref.y,},)if (skeletonRefPos === 1) {// 骨架参考位置为右侧(上侧)start = new FP.Joint('start',{x: start_ref.x,y: start_ref.y + weight / 2,},)end = new FP.Joint('end',{x: end_ref.x,y: end_ref.y + weight / 2,},)} else if (skeletonRefPos === 2) {// 骨架参考位置为左侧(下侧)start = new FP.Joint('start',{x: start_ref.x,y: start_ref.y - weight / 2,},)end = new FP.Joint('end',{x: end_ref.x,y: end_ref.y - weight / 2,},)} else {// 默认骨架参考位置,即骨架参考位置为中间实际绘制的骨架位置start = new FP.Joint('start',{x: start_ref.x,y: start_ref.y,},)end = new FP.Joint('end',{x: end_ref.x,y: end_ref.y,},)}glyph.addJoint(start_ref)glyph.addJoint(end_ref)glyph.addRefLine(refline(start_ref, end_ref, 'ref'))glyph.addJoint(start)glyph.addJoint(end)const skeleton = {start,end,}glyph.addRefLine(refline(start, end))const components = getComponents(skeleton, global_params)for (let i = 0; i < components.length; i++) {glyph.addComponent(components[i])}glyph.getSkeleton = () => {return skeleton}glyph.getComponentsBySkeleton = (skeleton) => {return getComponents(skeleton, global_params)}
}const getComponents = (skeleton, global_params) => {// 获取骨架以外的全局风格变量const { start_style_type, start_style_value, weight } = global_paramsconst getStartStyle = (start_style_type, start_style_value) => {if (start_style_type === 1) {// 起笔上下凸起长方形return {start_style_decorator_width: start_style_value * 20,start_style_decorator_height: weight * 0.25,}} else if (start_style_type === 2) {// 起笔上下凸起长方形,长方形内侧转角为圆角return {start_style_decorator_width: start_style_value * 20,start_style_decorator_height: weight * 0.25,start_style_decorator_radius: 20,}}return {}}const start_style = getStartStyle(start_style_type, start_style_value)// 根据骨架计算轮廓关键点const { start, end } = skeleton// out指上侧(外侧)轮廓线// in指下侧(内侧)轮廓线const { out_heng_start, out_heng_end, in_heng_start, in_heng_end } = FP.getLineContours('heng', { heng_start: start, heng_end: end }, weight)// 创建钢笔组件const pen = new FP.PenComponent()pen.beginPath()// 绘制横的上侧轮廓if (start_style_type === 0) {// 无起笔样式pen.moveTo(out_heng_start.x, out_heng_start.y)} else if (start_style_type === 1) {// 起笔上下凸起长方形pen.moveTo(out_heng_start.x, out_heng_start.y - start_style.start_style_decorator_height)pen.lineTo(out_heng_start.x + start_style.start_style_decorator_width, out_heng_start.y - start_style.start_style_decorator_height)pen.lineTo(out_heng_start.x + start_style.start_style_decorator_width, out_heng_start.y)} else if (start_style_type === 2) {// 起笔上下凸起长方形,长方形内侧转角为圆角pen.moveTo(out_heng_start.x, out_heng_start.y - start_style.start_style_decorator_height)pen.lineTo(out_heng_start.x + start_style.start_style_decorator_width, out_heng_start.y - start_style.start_style_decorator_height)pen.quadraticBezierTo(out_heng_start.x + start_style.start_style_decorator_width,out_heng_start.y,out_heng_start.x + start_style.start_style_decorator_width + start_style.start_style_decorator_radius,out_heng_start.y,)}pen.lineTo(out_heng_end.x, out_heng_end.y)// 绘制轮廓连接线pen.lineTo(in_heng_end.x, in_heng_end.y)// 绘制横的下侧轮廓if (start_style_type === 0) {// 无起笔样式pen.lineTo(in_heng_start.x, in_heng_start.y)} else if (start_style_type === 1) {// 起笔上下凸起长方形pen.lineTo(in_heng_start.x + start_style.start_style_decorator_width, in_heng_start.y)pen.lineTo(in_heng_start.x + start_style.start_style_decorator_width, in_heng_start.y + start_style.start_style_decorator_height)pen.lineTo(in_heng_start.x, in_heng_start.y + start_style.start_style_decorator_height)} else if (start_style_type === 2) {// 起笔上下凸起长方形,长方形内侧转角为圆角pen.lineTo(in_heng_start.x + start_style.start_style_decorator_width + start_style.start_style_decorator_radius,in_heng_start.y,)pen.quadraticBezierTo(in_heng_start.x + start_style.start_style_decorator_width,in_heng_start.y,in_heng_start.x + start_style.start_style_decorator_width,in_heng_start.y + start_style.start_style_decorator_height,)pen.lineTo(in_heng_start.x, in_heng_start.y + start_style.start_style_decorator_height)}// 绘制轮廓连接线if (start_style_type === 0) {// 无起笔样式pen.lineTo(out_heng_start.x, out_heng_start.y)} else if (start_style_type === 1) {// 起笔上下凸起长方形pen.lineTo(out_heng_start.x, out_heng_start.y - start_style.start_style_decorator_height)} else if (start_style_type === 2) {// 起笔上下凸起长方形,长方形内侧转角为圆角pen.lineTo(out_heng_start.x, out_heng_start.y - start_style.start_style_decorator_height)}pen.closePath()return [ pen ]
}updateGlyphByParams(params, global_params)

了解了笔画横的绘制过程,你是不是觉得使用程序绘制笔画非常简单?但其实在绘制过程中,我们还会遇到各种各样的细节问题,接下来笔者将简述一些绘制中遇到的细节问题。

撇的两侧轮廓绘制——不是两条贝塞尔曲线那么简单

在绘制笔画“撇”的时候,要绘制出字重的厚度需要绘制骨架两侧的轮廓。最直观的方式是将骨架关键点左移 weight / 2 作为左侧轮廓关键点,并右移 weight / 2 作为右侧轮廓关键点,再以轮廓关起点终点为锚点,拐点为控制点绘制成二次贝塞尔曲线。然后这样绘制出的两条轮廓,其实不是每个截段字重都相同,并且字重也并非每处都是 weight 不变。要解决这个问题,我们需要提取在骨架二次贝塞尔曲线上的离散点,比如,从起点到终点均匀提取100个离散点。然后,计算每个离散点在曲线上的切线,根据切线位置垂直移动 weight / 2 作为新轮廓上的离散点。最后,连新轮廓上的离散点拟合为新的贝塞尔曲线。这样,问题就圆满解决了。

转角的处理,怎样能显得不突兀?

一般来说,在绘制轮廓时,笔者会先根据骨架关键点生成外轮廓与内轮廓,然而当绘制转角时,直接使用两个衔接笔画的终点与起点是不行的,因为它们是错位的。比较直观的方式是分别计算两个衔接笔画外轮廓的直线交点和内轮廓的直线交点。但是这样当折笔旋转过大时,会显得非常突兀,这时候就需要给转角加一个切角。我们可以根据切角大小计算出相应关键点,对于直线轮廓,计算切角关键点比较简单,但对于曲线轮廓,求交点时需要取相应锚点的切线来计算计算。另外如果切角会扩大至曲线上,切角关键点则需要在曲线上取相应距离的点,会稍微麻烦一点。以笔画“横撇”为例,假如我们需要切一个大小为50的切角,对于笔画横,直接取 外轮廓终点 - 50 处的点为切点,而对于笔画撇,则需要先计算出外轮廓曲线上的离散点,然后遍历离散点,当离散点距离总和达到50时,取该处的点为切点。

笔画骨架关键点的拖拽编辑

除了改变“长度”等直观参数,字玩同时支持用户通过拖拽骨架关键点的方式改变骨架。这时候,有个关键的问题就是当骨架改变之后,需要根据骨架重新计算“长度”等直观参数。对于“横”、“竖”、“折”来说,这个计算相对简单,然而对于“撇”、“捺”、“点”、“挑”来说,会稍微复杂一点——因为我们需要根据拐点位置计算出垂线长度与垂足位置,这需要一点基础数学知识。同时,在根据骨架关键点计算直观参数的时候,还需要特别注意参数边界的处理——用户很可能在拖拽过程中,将骨架关键点拖拽到边界以外的地方,这时候,需要自动将参数设置在边界处以防止字形绘制错误。

未来展望

基于黑体的笔画绘制与字形组装只是数字化、参数化探索中很基础的一步尝试,未来还有很多工作可以做。笔者希望首先完善黑体笔画的绘制,比如丰富风格参数,支持更多的笔画风格。这其中很有探索空间的地方就是“字重变化”。目前“字重变化”的设计比较简陋,但是通过对字重进行单元贝塞尔曲线的叠乘,使用类似缓动函数的原理,可以将笔画粗细变化调成任意风格,尽管还需要解决很多细节问题,但这将是个很有趣的数字化尝试。另外,笔者也希望将研究拓展到黑体以外,比如尝试宋体或楷体的笔画绘制。同时,图标字体也是笔者非常感兴趣的方向,字玩除了文字也支持图标绘制。图标通常形状更加简易,相对容易抽象出参数,在未来笔者也希望尝试使用程序绘制一些简单图标,制作一些基本图标的可调参模板。笔者水平非常有限,在探索过程中也经常出现力不从心的情况,写下此文也希望抛砖引玉,让更多朋友参与到中文字体数字化、参数化的研究探索中,为字体设计行业添砖加瓦。

相关文章:

使用程序绘制中文字体——中文字体的参数化设计方案初探

目录 写在前面基本设计思路笔画骨架参数设计笔画风格参数设计起笔风格转角风格字重变化弯曲程度 字形的“组装拟合”基于骨架的结构调整笔画绘制二三事撇的两侧轮廓绘制——不是两条贝塞尔曲线那么简单转角的处理&#xff0c;怎样能显得不突兀&#xff1f;笔画骨架关键点的拖拽…...

高频数据结构面试题总结

基础数据结构 1. 数组(Array) 特点&#xff1a;连续内存、固定大小、随机访问O(1)常见问题&#xff1a; 两数之和/三数之和合并两个有序数组删除排序数组中的重复项旋转数组最大子数组和(Kadane算法) 2. 链表(Linked List) 类型&#xff1a;单链表、双链表、循环链表常见问…...

工业设计破局密码:3D 可视化技术点燃产业升级引擎

3D可视化是一种将数据、信息或抽象概念以三维图形、模型和动画的形式呈现出来的技术。3D可视化技术通过构建三维数字孪生体&#xff0c;将设计思维转化为可交互的虚拟原型&#xff0c;不仅打破了传统二维设计的空间局限&#xff0c;更在效率、精度与用户体验层面开创了全新维度…...

【动态导通电阻】p-GaN HEMTs正向和反向导通下的动态导通电阻

2024 年,浙江大学的 Zonglun Xie 等人基于多组双脉冲测试方法,研究了两种不同技术的商用 p-GaN 栅极 HEMTs 在正向和反向导通模式以及硬开关和软开关条件下的动态导通电阻(RON)特性。实验结果表明,对于肖特基型 p-GaN 栅极 HEMTs,反向导通时动态 RON 比正向导通高 3%-5%;…...

Python代码编程基础

字符串 str.[]实现根据下标定位实现对元素的截取 for 循环可以实现遍历 while 循环可以在实现遍历的同时实现对某一下标数值的修改 字符串前加 r 可以实现对字符串的完整内容输出 字符串前加 f 可以实现对字符串内{}中包裹内容的格式化输出&#xff0c;仅在 v3.6 之后可用…...

基于RAG+MCP开发【企文小智】企业智能体

一、业务场景描述 1.1、背景介绍 几乎每家企业都积累了大量关于规章制度的文档资料&#xff0c;例如薪酬福利、绩效考核、保密协议、考勤管理、采购制度、资产管理制度等。这些文档大多以 Word、PDF 等非结构化格式存在。传统方式下&#xff0c;员工在查询某项具体规则时&…...

【软件测试】测试用例的设计方法

目录 一、基于需求进行测试用例的设计 1.1 功能需求测试分析 二、黑盒测试用例设计方法 2.1 等价类划分法(解决穷举) 2.1.1 等价类设计步骤 2.1.2 等价类划分法案例 2.1.2.1 验证 QQ 账号的合法性 2.1.2.2 验证某城市电话号码的正确性 2.1.3 适用场景 2.2 边界值分析…...

计算机网络笔记(十八)——3.5高速以太网

3.5.1 100BASE-T以太网 1. 基本概念 标准规范&#xff1a;IEEE 802.3u&#xff0c;是快速以太网的典型代表&#xff0c;运行速率100Mbps。物理介质&#xff1a;使用双绞线&#xff08;UTP或STP&#xff09;&#xff0c;支持最大传输距离100米&#xff08;Cat5/5e及以上&#…...

海外广告账号资源解析:如何选择适合业务的广告账户?

在全球化数字营销的浪潮下&#xff0c;海外广告投放已成为企业拓展市场的核心手段。然而&#xff0c;不同平台的广告账号类型复杂多样&#xff0c;如何选择适合自身业务的资源&#xff1f;本文将深度解析 Facebook、Google、TikTok 三大平台的广告账号类型&#xff0c;助您精准…...

Java设计模式之建造者模式:从入门到精通

1. 建造者模式概述 1.1 定义与核心概念 **建造者模式(Builder Pattern)**是一种创建型设计模式,它将复杂对象的构建过程与其表示分离,使得同样的构建过程可以创建不同的表示。 专业术语解释表: 术语解释产品(Product)最终要构建的复杂对象建造者(Builder)定义创建产品各个…...

Faiss 索引深度解析:从基础到实战

在处理高维数据的相似性搜索时&#xff0c;Faiss&#xff08;Facebook AI Similarity Search&#xff09;无疑是一款强大且高效的工具。它为我们提供了多种索引类型&#xff0c;适用于不同规模和需求的数据场景。本文将结合代码实例&#xff0c;深入剖析 Faiss 中常见索引的原理…...

Error parsing column 10 (YingShou=-99.5 - Double) dapper sqlite

在使用sqlite 调取 dapper的时候出现这个问题提示&#xff1a; 原因是 在 sqlite表中设定的字段类型是 decimel而在C#的字段属性也是decimel&#xff0c;结果解析F负数 小数的时候出现这个错误提示&#xff1a; 解决办法&#xff1a;使用默认的sqlite的字段类型来填入 REAL描述…...

星云智控:物联网时代的设备守护者——卓伊凡详解物联网监控革命-优雅草卓伊凡

星云智控&#xff1a;物联网时代的设备守护者——卓伊凡详解物联网监控革命-优雅草卓伊凡 一、物联网的本质解析 1.1 什么是物联网&#xff1f; 当卓伊凡被问及”星云智控物联网是干嘛的”这个问题时&#xff0c;他首先给出了一个技术定义&#xff1a;物联网&#xff08;Int…...

LeRobot 项目部署运行逻辑(五)——intelrealsense.py/configs.py

在运行 control_robot.py 的时候会启用相机拍摄&#xff0c;lerobot 中封装好了两种相机类型&#xff1a;realsense 和 opencv realsense 直接使用他们的脚本就可以&#xff0c;但需要在 lerobot/robot_devices/robots/configs.py 中修改相机 serial_number 由于我们设备采用的…...

从0开始学linux韦东山教程第一三章问题小结(1)

本人从0开始学习linux&#xff0c;使用的是韦东山的教程&#xff0c;在跟着课程学习的情况下的所遇到的问题的总结,理论虽枯燥但是是基础。 摘要关键词&#xff1a;VMware、Ubuntu、网络网口 视频链接&#xff1a;【【韦东山】韦东山手把手教你嵌入式Linux快速入门到精通 | Lin…...

解决 MySQL 数据库无法远程连接的问题

在使用 MySQL 数据库时&#xff0c;遇到这样的问题&#xff1a; 本地可以连接 MySQL&#xff0c;但远程机器连接时&#xff0c;总是报错 Host ... is not allowed to connect to this MySQL server。 这通常是因为 MySQL 的用户权限或配置限制了远程访问。 1. 登录 MySQL 数据…...

分享一款开源的图片去重软件 ImageContrastTools,基于Electron和hash算法

最近发现个挺实在的图片查重软件&#xff0c;叫ImageContrastTools。电脑手机都能用&#xff0c;特别适合整理乱七八糟的相册。直接去这里下载就能用&#xff1a; https://github.com/html365/ImageContrastTools 功能说明&#xff1a; 1️⃣ 选个文件夹就能自动扫重复图&…...

软件测试——用例篇(2)

目录 一、基于需求的设计方法 1.1设计账号注册、账号登录的测试用例 1.1.1功能测试 1.1.2界面测试 1.1.3性能测试 1.1.4兼容性测试 1.1.5易用性测试 1.1.6安全测试 一、基于需求的设计方法 根据参考需求文档/产品规格说明书来设计测试用例 测试人员接到需求之后、对需求…...

图像匹配导航定位技术 第 11 章

第 11 章 基 于 改 进 SIFT 的 SAR 与 可 见光 图 像 匹 配 控 制 点 定 位 算 法 HOG 描述子也只是对整幅图像的特征向量进行匹配&#xff0c;但是仍然存在局部匹配误差。而局部不变特征&#xff08;如 SIFT,Harris 等&#xff09;是对特征点局部邻域的特征进行描述来构造局部…...

安装jdk步骤

将Linux安装jdk的步骤放入shell脚本中 #!/bin/bash # 阿里云服务器专用 - 全自动安装 OpenJDK 1.8&#xff08;无交互&#xff09; # 仅支持 yum 系系统&#xff08;CentOS/RHEL/Alibaba Cloud Linux&#xff09;# 检查 root 权限 if [ "$(id -u)" -ne 0 ]; thenech…...

理解 `.sln` 和 `.csproj`:从项目结构到构建发布的一次梳理

理解 .sln 和 .csproj&#xff1a;从项目结构到构建发布的一次梳理 在初学 .NET 项目开发时&#xff0c;很多人都会对 .sln&#xff08;解决方案&#xff09;和 .csproj&#xff08;项目&#xff09;文件感到疑惑。随着开发经验的积累&#xff0c;我逐渐理解了这些层级的设计意…...

高频算法面试题总结

高频算法面试题总结 排序算法 1. 基础排序算法 快速排序: public void quickSort(int[] arr, int low, int high) {if (low < high) {int pivot = partition(arr, low, high);quickSort(arr, low, pivot - 1);quickSort(arr, pivot + 1, high);} }平均时间复杂度:O(n lo…...

SQL进阶:如何把字段中的键值对转为JSON格式?

JSON 一、问题描述二、ORACLE<一>、键值对拆分(REGEXP_SUBSTR)<二>、转为JSON<三>、不足 三、MYSQL<一>、键值对拆分(RECURSIVE)<二>、转为JSON 一、问题描述 假如某张表的某列是键值对数据,如何把这个键值对转为json格式,数据如下所示 dynast…...

vue3:十二、图形看板- echart图表-柱状图、饼图

一、效果 如图展示增加了饼图和柱状图,并且优化了浏览器窗口大小更改,图表随着改变 二、 饼图 1、新建组件文件 新增组件EchartsExaminePie.vue,用于存储审核饼图的图表 2、写入组件信息 (1)视图层 写入一个div,写入变量chart和图表宽高 <template><div ref…...

nacos-server-2.2.2.tar及使用方式

下载链接 nacos-server-2.2.2.tar包及使用资源-CSDN文库 下载与安装 下载地址&#xff1a;可从 Nacos 官网版本下载页面 或 Nacos GitHub Releases 获取 nacos-server-2.2.2.tar.gz 安装包。 环境准备&#xff1a;Nacos 依赖 Java 环境运行&#xff0c;需确保安装了 64 位 J…...

el-form的label星号位置如何修改

默认情况 修改后 实现代码 .el-form {.el-form-item {.el-form-item__label {padding: 0;&::before {float: none;position: relative;}}} }...

小刚说C语言刷题—1004阶乘问题

1.题目描述 编程求 123⋯n 。 输入 输入一行&#xff0c;只有一个整数 n(1≤n≤10)&#xff1b; 输出 输出只有一行&#xff08;这意味着末尾有一个回车符号&#xff09;&#xff0c;包括 1 个整数。 样例 输入 5 输出 120 2.参考代码(C语言版) #include <stdio…...

Java 集合体系深度解析面试篇

一、Java 集合体系核心架构与高频考点 1. 集合体系架构图&#xff08;大厂必问&#xff09; Java集合框架 ├─ Collection&#xff08;单列集合&#xff09; │ ├─ List&#xff08;有序、可重复&#xff09; │ │ ├─ ArrayList&#xff08;动态数组&#xff0c;随机…...

websocketd 10秒教程

websocketd 参考地址&#xff1a;joewalnes/websocketd 官网地址&#xff1a;websocketd websocketd简述 websocketd是一个简单的websocket服务Server&#xff0c;运行在命令行方式下&#xff0c;可以通过websocketd和已经有程序进行交互。 现在&#xff0c;可以非常容易地构…...

PCA降维

主成分分析&#xff08;Principal Component Analysis&#xff0c;PCA&#xff09;降维是一种广泛使用的无监督机器学习技术&#xff0c;主要用于数据预处理阶段&#xff0c;其目的是在尽量保留数据重要信息的前提下&#xff0c;减少数据的维度。 PCA 的原理​ PCA 的核心思想…...

【计算机视觉】OpenCV实战项目: opencv-text-deskew:实时文本图像校正

opencv-text-deskew&#xff1a;基于OpenCV的实时文本图像校正 一、项目概述与技术背景1.1 核心功能与创新点1.2 技术指标对比1.3 技术演进路线 二、环境配置与算法原理2.1 硬件要求2.2 软件部署2.3 核心算法流程 三、核心算法解析3.1 文本区域定位3.2 角度检测优化3.3 仿射变换…...

具身智能时代的机器人导航和操作仿真器综述

系列文章目录 前言 导航和操作是具身智能的核心能力&#xff0c;然而在现实世界中训练具有这些能力的智能体却面临着高成本和时间复杂性。因此&#xff0c;从模拟到现实的转移已成为一种关键方法&#xff0c;但模拟到现实的差距依然存在。本调查通过分析以往调查中忽略的物理模…...

Go语言Stdio传输MCP Server示例【Cline、Roo Code】

Go语言 Stdio 传输 MCP Server 示例 AI 应用开发正处于加速发展阶段&#xff0c;新技术和新方法不断涌现。Model Context Protocol &#xff08;MCP&#xff09; 作为一个开放标准&#xff0c;正在改变 AI 应用与数据源和工具集成的方式。 Go-MCP 是一个 MCP 协议的 GO 实现&…...

Xcode16.3配置越狱开发环境

首先先在https://developer.apple.com/xcode/resources/ 这里面登陆Apple账号&#xff0c;然后访问url下载 https://download.developer.apple.com/Developer_Tools/Xcode_16.3/Xcode_16.3.xip 1、安装theos https://theos.dev/docs/installation-macos 会安装到默认位置~/th…...

AWS IoT Core与MSK跨账号集成:突破边界的IoT数据处理方案

随着企业规模的扩大和业务的复杂化,跨账号资源访问成为云架构中的一个常见需求。本文将深入探讨如何实现AWS IoT Core与Amazon MSK(Managed Streaming for Apache Kafka)的跨账号集成,为您的IoT数据处理方案开辟新的可能性。无论您是正在构建多账号架构,还是需要整合不同部门的…...

【Python 列表(List)】

Python 中的列表&#xff08;List&#xff09;是最常用、最灵活的有序数据集合&#xff0c;支持动态增删改查操作。以下是列表的核心知识点&#xff1a; 一、基础特性 有序性&#xff1a;元素按插入顺序存储可变性&#xff1a;支持增删改操作允许重复&#xff1a;可存储重复元…...

在另一个省发布抖音作品,IP属地会随之变化吗?

你是否曾有过这样的疑惑&#xff1a;出差旅游时在外地发布了一条抖音视频&#xff0c;评论区突然冒出“IP怎么显示xx省了&#xff1f;”的提问&#xff1f;随着各大社交平台上线“IP属地”功能&#xff0c;用户的地理位置标识成为公开信息&#xff0c;而属地显示的“灵敏性”也…...

在线工具源码_字典查询_汉语词典_成语查询_择吉黄历等255个工具数百万数据 养站神器,安装教程

在线工具源码_字典查询_汉语词典_成语查询_择吉黄历等255个工具数百万数据 养站神器&#xff0c;安装教程 资源宝分享&#xff1a;https://www.httple.net/154301.html 一次性打包涵盖200个常用工具&#xff01;无论是日常的图片处理、文件格式转换&#xff0c;还是实用的时间…...

D720201 PCIE 转USB HUB

1. 启动时出现了下面错误 [ 4.682595] pcieport 0004:00:00.0: Signaling PME through PCIe PME interrupt [ 4.684939] pci 0004:01:00.0: Signaling PME through PCIe PME interrupt [ 4.691287] pci 0004:01:00.0: enabling device (0000 -> 0002) [ 5.2962…...

QT事件介绍及实现字体放大缩小(滚轮)

使用update是为了回调paintEvent这个事件函数 pic.load是加载一张图片 setfixedsize(pic.siez())是为了把按键的矩形区域变成和pic一样大 painter.drawPixmap(rec(),pic)就是在按键的矩形区域画一个pic emit clicked();是用来发送clicked信号的&#xff0c;当然你也可以在事…...

p2p虚拟服务器

ZeroTier Central ✅ 推荐工具&#xff1a;ZeroTier&#xff08;免费、稳定、跨平台&#xff09; ZeroTier 可以帮你把多台设备&#xff08;无论是否跨网&#xff09;加入一个虚拟局域网&#xff0c;彼此间可以像在同一个 LAN 中通信&#xff0c;UDP 视频、文件传输、SSH 等都…...

高尔夫基本知识及规则·棒球1号位

高尔夫与棒球的结合看似跨界&#xff0c;但两者在规则、策略和运动哲学上存在有趣的关联性。以下从五个角度进行对比分析&#xff0c;揭示它们的异同与潜在联系&#xff1a; 一、核心目标的对比性结合 高尔夫&#xff1a;以最少击球次数完成18洞&#xff08;标准杆72杆左右&am…...

数据结构(1)复杂度

一、数据结构概要 1.数据结构 数据结构是计算机存储、组织数据的方式&#xff0c;是数据相互之间存在一种或者多种特定关系的集合。没有一种单一的数据结构可以解决所有问题&#xff0c;因此要学习多种多样的数据结构。如&#xff1a;线性表、图、树等。 2.算法 算法其实就…...

多分类问题softmax传递函数+交叉熵损失

在多分类问题中&#xff0c;Softmax 函数通常与交叉熵损失函数结合使用。 Softmax 函数 Softmax 函数是一种常用的激活函数&#xff0c;主要用于多分类问题中。它将一个实数向量转换为概率分布&#xff0c;使得每个元素的值在 0 到 1 之间&#xff0c;且所有元素的和为 1。 …...

Java如何获取电脑分辨率?

以下是一个 Java 程序示例&#xff0c;用于获取电脑的主屏幕分辨率&#xff1a; import java.awt.*; public class ScreenResolutionExample { public static void main(String[] args) { // 获取默认的屏幕设备 GraphicsDevice device GraphicsEnvironm…...

【NextPilot日志移植】logged_topics.cpp解析

&#x1f4d8; PX4 Logger 模块注册 uORB 主题、实际订阅与数据采集流程 &#x1f9ed; 目的与背景 在 PX4 飞控中&#xff0c;日志记录模块 logger 需要记录多个 uORB 主题的数据&#xff08;如 IMU、GPS、姿态等&#xff09;。为了系统统一管理这些记录需求&#xff0c;log…...

CSS vertical-align

这里的小空白就是为了和基线对齐 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta http-equiv"X-UA-Compatible" content"IEedge"><meta name"viewport" content&quo…...

“工作区”升级为“磁盘”、数据集统计概览优化|ModelWhale 版本更新

本次更新围绕用户在实际项目中对平台的理解和管理体验进行了多项优化。 “工作区”升级为“磁盘”、及其管理优化 平台“工作区”概念正式更名为“磁盘”&#xff0c;突出其存储功能。原有以目录代称的存储区域划分同步更名&#xff0c;其中“work目录”更改为“个人磁盘”&am…...

mac 电脑如何打开剪切板

mac 不像 Windows 拥有官方的剪贴板应用。所以我们需要使用官方商店中的第三方应用实现剪切板管理的功能。 打开苹果电脑上的 App Store&#xff0c;下载 PasteMe 或 Paste 等复制历史记录的管理工具。&#xff08;PasteMe 为买断制18元&#xff0c;Paste为订阅制&#xff0c;…...

信息系统项目管理师-软考高级(软考高项)​​​​​​​​​​​2025最新(十二)

个人笔记整理---仅供参考 第十二章项目质量管理 可能考论文&#xff1f;&#xff1f;&#xff1f; 12.1管理基础 12.2项目质量管理过程 12.3规划质量管理 12.4管理质量 12.5控制质量...