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

C# 程序实现:软头丙烯马克笔 笔迹效果模拟(一) - 行人-

​吸引人的画笔效果

1

小的时候,从来没有使用过 软头丙烯马克笔,有一次去文具店, 我被画笔鲜亮的颜色所吸引,售货员告诉我,这个笔写的字,不会在纸张上有任何的渗透,而且是软头的,我心想,软头的?那不是写起字来像毛笔的感觉吗?而且竟然不渗透纸张?这么神奇,于是买了回来,哇~ 写字的体验真的太棒了,写出来的字,圆圆的,很饱满,略带毛笔的渗透边缘的感觉,还会错以为自己写字很好而爱上写字,好像给字体加上了美颜的效果,真的太喜欢了。

所以我在想:软件上在进行白板签名的时候,能不能也显示类似的效果呢?于是我打算尝试一下, 看看能不能签名的时候模拟出类似的效果, 哪怕有一点点的相似呢?


寻找参考 – Ougishi

首先,我先进行各种搜罗, 看是否已经有类似 笔迹优化 软件,进行了各种搜索之后,找到了一个名字为  ougishi.exe 的软件,这个软件的表现真的很让我吃惊,写出来的字体真的有行云流水的流畅丝滑的感觉,但是它有两个缺点:字体的效果不是实时的,写完之后要点击一下 变换 显示,也有可能是版本的限制吧,除了这一点,其他的真的是看起来很完美:

2

3


寻找参考 – Win10 画图

另外,Win10系统自带的绘画软件也有一些效果,它的自由线条画笔有9种效果,但是看起来真的很不美丽:

4、

贝塞尔曲线画笔 有6种:

5


寻找参考 – WPF InkCanvas

另外WPF自带的的 InkCanvas 画板效果,看起来非常的流畅, 它在绘制的 过程中 线条是有折线感的,在绘制完成之后,立刻自动进行了优化,所以看起来整体上也基本是实时的。它最大的缺点 就是所有点的位置都是均匀的,除此之外,其他都非常棒,极其流畅自然 :

功能代码:

XAML:
<ink:InkCanvas x:Name="signatureCanvas" DefaultDrawingAttributes="{StaticResource DefaultDrawingAttributes}"/>XAML.CS:
signatureCanvas.DefaultDrawingAttributes.Color = Colors.Red;
signatureCanvas.DefaultDrawingAttributes.Width = 30;
signatureCanvas.DefaultDrawingAttributes.Height = 30;
signatureCanvas.DefaultDrawingAttributes.FitToCurve = true;
signatureCanvas.EditingMode = InkCanvasEditingMode.Ink;

6

目前我能找到比较好的效果,就以上这些,接下来我打算一步步进行尝试 。


从零开始尝试

根据查阅的资料,我了解到,在手写签名相关的设备上,有一些硬件是支持感应笔的压力,转弯的角度 等这些 与真实笔 接近的物理特性,但是我目前手头只有一个普通的触摸屏,它除了收集触摸的点的信息之外,没有任何其他数据可参考,因此我打算研究的前提是:普通的鼠标绘制,或者是普通的触摸屏绘制,因此我能依赖的信息,只有绘制在屏幕上的点,由此可以关联得到的信息是:根据点的记录时间来得到点的绘制速度,因此我研究的效果中,影响签名笔迹效果的是,每个点处的速度,根据不同的点速度显示为不同的形态

尝试1:观察点规律

那么在屏幕上签名的时候,实际补捉到的点,到底是什么样的形态呢 ?

先来收集点:

private bool is_down = false;
private Dictionary<PointF, DateTime> pt_time_dict = new Dictionary<PointF, DateTime>();
private void panel1_MouseDown(object sender, MouseEventArgs e)
{
    is_down = true;
    pt_time_dict.Clear();
    pt_time_dict[new PointF(e.Location.X, e.Location.Y)] = DateTime.Now;
}
private void panel1_MouseMove(object sender, MouseEventArgs e)
{
    if (!is_down) return;
    pt_time_dict[new PointF(e.Location.X, e.Location.Y)] = DateTime.Now;
    panel1.Invalidate();
}
private void panel1_MouseUp(object sender, MouseEventArgs e)
{
    is_down = false;
}
private void panel1_Paint(object sender, PaintEventArgs e)
 {
     var keys = pt_time_dict.Keys.ToList(); 
     for (int i = 0; i < keys.Count ; i++)
     {
         var rt = new System.Drawing.RectangleF(keys[i],new SizeF());
         rt.Inflate(5, 5);
         e.Graphics.FillEllipse(Brushes.Red, rt);
     }
 }

7

我们发现,在每一个点的位置,都绘制一个圆圈的话,线条的所有的位置看起来粗细都是相同的,只有在笔画 有交叉重叠的位置会显得更加的粗一些,但是实际上,我用马克笔以比较慢的速度绘制的时候,所有位置看起来都是大致粗细均衡的,但是当我在某个位置快速滑过去的时候,在结尾处,它有一个非常漂亮的尾巴,线条趋于变细,并且有稍微分叉的感觉,而且由于我有一种把笔画提过去冲劲儿,所以在提笔的时候,一般会有一点蓄力的感觉,所以会停顿的稍微久一点点,看到的颜色会更深,笔墨更饱满,笔划更粗,在笔画结束的位置,笔会逐渐抬起并离开纸张,接触纸的面积越来越小,所以笔画会越来越细:

8


尝试2:观察点速度

这里描述的特性稍微有一点点多 ,我无法一下全部关注它们。首先,我希望先了解每个点的速度,速度 等于 距离 除以 时间 ,所以我计划把每一点的速度 = 它与下一个点的距离 除以 它与下一点的时间差 ,单位分别是 像素 和 秒 :

绘制代码:

private void panel1_Paint1(object sender, PaintEventArgs e)
{
    var keys = pt_time_dict.Keys.ToList();
    var value = pt_time_dict.Values.ToList();
    var speeds = new List<double>();
    for (int i = 0; i < keys.Count - 1; i++)
    {
        var rt = new System.Drawing.RectangleF(keys[i], new SizeF());
        rt.Inflate(5, 5);
        e.Graphics.FillEllipse(Brushes.Red, rt);
        var dist = ptDist(keys[i], keys[i + 1]);
        var time = (value[i + 1] - value[i]).TotalSeconds;
        var speed = dist / time;
        speeds.Add(speed);
    } 
    Debug.WriteLine("速度:" + string.Join("、", speeds.Select(a=>(int)a)));
}

9


尝试3:让速度影响点大小

忽略过高或过低的异常值,我们观察到,绘制速度 快和慢的情况下,范围 大概在50 到 10000之间 ,假设我们设置最高速度为5000,最低速度为50 ,它们对应到的笔画的粗细,分别映射到 最细为1个像素, 最粗为30个像素:

绘制代码:

private void panel1_Paint (object sender, PaintEventArgs e)
{
    var keys = pt_time_dict.Keys.ToList();
    var value = pt_time_dict.Values.ToList();
    var speedMax = 5000;
    var speedMin = 50;
    var speedRange = speedMax - speedMin;
    var radusMin = 1;
    var radusMax = 30;
    var radusRange = radusMax - radusMin;
    e.Graphics.Clear(Color.White);
    List<float> lst_radus = new List<float>();
    for (int i = 0; i < keys.Count; i++)
    {
        var speed = 0d;
        if (i == keys.Count - 1)
        {
            var dist = ptDist(keys[i], keys[i - 1]);
            var time = (value[i] - value[i - 1]).TotalSeconds;
            speed = Math.Min(speedMax, dist / time);
        }
        else
        {
            var dist = ptDist(keys[i], keys[i + 1]);
            var time = (value[i + 1] - value[i]).TotalSeconds;
            speed = Math.Min(speedMax, dist / time);
        }
        var pct = 1 - (speed - speedMin) / (float)speedRange;
        var r = (float)Math.Round(pct * radusRange + radusMin);
        r = Math.Max(r, 1);
        lst_radus.Add(r);
    }
    //绘制 
    e.Graphics.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
    for (int i = 0; i < keys.Count - 1; i++)
    {
        var r = lst_radus[i];
        var rt = new System.Drawing.RectangleF(keys[i], new SizeF());
        rt.Inflate(r, r);
        e.Graphics.FillEllipse(Brushes.Red, rt);
    }
}

10

这个时候,我们看到:在慢速绘制的时候,圆圆的,有可爱的感觉了,在快速绘制的时候,也能够看到粗细的区别了。


尝试4:让线条更平滑

但是有一些问题,在快速绘制的时候,相邻的两个点大小差异太大,而且点的位置,有点乱,很不平滑,因此 因此需要添加对它们的平滑处理。

对于每个点的大小, 由与它相邻的前后两个点的大小做平均平均值,每个点的位置也由它前一点和后一点的x和y的均值来得到,这是1次平滑的计算,我们进行连续10次平滑计算,看一下效果:

平滑处理代码:

private void panel1_Paint (object sender, PaintEventArgs e)
{
    var keys = pt_time_dict.Keys.ToList();
    var value = pt_time_dict.Values.ToList();
    var speedMax = 5000;
    var speedMin = 50;
    var speedRange = speedMax - speedMin;
    var radusMin = 1;
    var radusMax = 30;
    var radusRange = radusMax - radusMin;
    e.Graphics.Clear(Color.White);
    List<float> lst_radus = new List<float>();
    for (int i = 0; i < keys.Count; i++)
    {
        var speed = 0d;
        if (i == keys.Count - 1)
        {
            var dist = ptDist(keys[i], keys[i - 1]);
            var time = (value[i] - value[i - 1]).TotalSeconds;
            speed = Math.Min(speedMax, dist / time);
        }
        else
        {
            var dist = ptDist(keys[i], keys[i + 1]);
            var time = (value[i + 1] - value[i]).TotalSeconds;
            speed = Math.Min(speedMax, dist / time);
        }
        var pct = 1 - (speed - speedMin) / (float)speedRange;
        var r = (float)Math.Round(pct * radusRange + radusMin);
        r = Math.Max(r, 1);
        lst_radus.Add(r);
    }
    for (int t = 0; t < 10; t++)
    {
        //平滑位置
        var lst_loc_rst = new List<PointF>();
        for (int i = 0; i < keys.Count; i++)
        {
            if (i == 0 || i == keys.Count - 1)
            {
                lst_loc_rst.Add(keys[i]);
                continue;
            }
            lst_loc_rst.Add(new PointF(
                (keys[i - 1].X + keys[i ].X + keys[i + 1].X) / 3f,
                (keys[i - 1].Y + keys[i ].Y + keys[i + 1].Y) / 3f
            ));
        }
        keys = lst_loc_rst;
        //平滑大小
        var lst_radus_rst = new List<float>();
        for (int i = 0; i < lst_radus.Count; i++)
        {
            if (i == 0 || i == lst_radus.Count - 1)
            {
                lst_radus_rst.Add(lst_radus[i]);
                continue;
            }
            lst_radus_rst.Add(
                (lst_radus[i - 1] + lst_radus[i]+ lst_radus[i + 1]) / 3f
             );
        }
        lst_radus = lst_radus_rst;
    }
    //绘制 
    e.Graphics.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
    for (int i = 0; i < keys.Count - 1; i++)
    {
       //以点为中心绘制 
       //var r = lst_radus[i]; 
       //var rt = new System.Drawing.RectangleF(keys[i], new SizeF()); 
       //rt.Inflate(r, r); 
       //e.Graphics.FillEllipse(Brushes.Red, rt); 
       //以点为左上点绘制 
       var r = lst_radus[i];  
       var rt = new System.Drawing.RectangleF(keys[i], new SizeF(r,r)); 
       e.Graphics.FillEllipse(Brushes.Red, rt);   }
}

11

我们可以看到,经过平滑处理后,在慢速绘制的时候看起来非常漂亮,尤其是以点为 左上 绘制的时候,起笔的笔锋位置,非常有感觉;但是在快速绘制的时候,有效点断开了,接下来我们要把点进行补满。


尝试5:补齐线条中的缺失点

在补满点之前,正常绘制一笔(或者偏慢),一笔大概是30个点到50个点 ,但是现在 补充点之后,点数增加到了500个点左右,此时绘制的时候,明显速度变慢,因为圆圈数量过多导致ui卡顿 从而影响页面点采集,导致实际采集点数只有10点左右,甚至更少,补齐后的线条也呈现出明显的线性特征,显得非常僵硬,并且绘制的时候严重闪烁,

12

这个时候有 3 个方案可以考虑:

方案 1 :使用鼠标钩子,将点的采集 与点的补齐算法、和ui绘制进行分离,使原始点的采集完全不受任何卡顿的影响,我对这种方案进行了尝试,是基本可行的,但是要处理相对位置(鼠标钩子是相对屏幕全局的位置),而且还要使用钩子机制,我个人不太倾向于这个方案;

方案 2 :进行离屏绘制,仍然是通过ui采集点,但是采集完成之后,对点的补齐和绘制在另外一个线程处理,UI重绘的时候直接绘制这个缓存图, 这个方案我也进行了尝试,但是会闪烁严重,并没有明显的提速;

方案 3 :这是一个设置起来很简单的方案,其实它类似于批量提交UI更新, 就是开启双缓冲机制, 实验证明:这是一个既简单又靠谱的方案,效果非常的好:

开启双缓冲,同时将点的计算放在单独的线程处理(点的补齐算法让AI写的,调教多次后出来的),然后调整显示方式使它错位显示,便于比对:

private void panel1_Paint(object sender, PaintEventArgs e)
{//检测双缓冲if (!enbale_doublebufferd)panel1.GetType().GetProperty("DoubleBuffered", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic).SetValue(panel1, true, null);//单独线程计算补齐点CheckPtsLoop();e.Graphics.Clear(Color.White);e.Graphics.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;stopwatch.Restart();foreach (var r in darw_rect_fill){e.Graphics.FillEllipse(Brushes.Red, r);}stopwatch.Stop();Debug.WriteLine("绘制补充点耗时毫秒:" + stopwatch.ElapsedMilliseconds);stopwatch.Restart();foreach (var r in darw_rect_init){e.Graphics.FillEllipse(Brushes.Green, r);}stopwatch.Stop();Debug.WriteLine("绘制原点耗时毫秒:" + stopwatch.ElapsedMilliseconds);if (true){//错位绘制,用于比对效果 - 原始 foreach (var r in darw_rect_init) e.Graphics.FillEllipse(Brushes.Red, r.X, r.Y - 150, r.Width, r.Height);//错位绘制,用于比对效果 - 合成{foreach (var r in darw_rect_fill) e.Graphics.FillEllipse(Brushes.Green, new RectangleF(r.X, r.Y + 150, r.Width, r.Height));foreach (var r in darw_rect_init) e.Graphics.FillEllipse(Brushes.Green, r.X, r.Y + 150, r.Width, r.Height);}}Text = "原点数:" + darw_rect_init.Count + ",补充后:" + darw_rect_fill.Count;
}

(每个图片中展示的3个分别为原始点,原始点+补充点分色显示、融合显示 )

13

14

开启了双缓冲之后的效果非常神奇,慢速绘制的效果非常好看,有粗有细,起笔有锋,这完全只根据了点的速度出来的效果,但是也明显地看到了一些问题:

问题 1:在非常快速绘制的时候,当前是按照 平均位置 来补齐的点,呈现出明显的线段形态,这里应该优化为贝塞尔曲线,

问题 2:目前只能看到最简单的笔锋效果,但实际上 软头丙烯马克笔 在进行绘制的时候,在不同笔画中的位置, 所呈现的形态是不一样的。比如我们在写笔画 捺 的时候,在拖尾的地方,笔的倾斜方向基本上是固定的,在绘制 捺 和 撇 的时候,尾巴的地方并不是像上面图片中简单的拖个细尾巴,这样其实不好看,它有一个 更浑厚饱满的尾巴,最后再加上一个小尖尾峰,稍微有一点毛笔感觉的样子。


因此接下来要做的工作是,在简单笔画的条件下,对不同的单笔画,加入一些特性的模拟。

等待后续更新。

http://www.sczhlp.com/news/62774/

相关文章:

  • 推荐系统三大挑战与前沿技术解析
  • 英语_错题集_2025-09
  • 互联网官方网站网络品牌推广策略
  • 土特产网站模板微信模板图片
  • 网站高级感网站底部分享怎么做
  • wordpress 双主题深圳优化网站
  • 北京建网站价格优帮云交易猫假网站制作
  • 企业招聘网站模板网站建设公司人员工资
  • 怎样在文章后做网站链接盐城网站开发代理咨询
  • 做网站时如何上传图片做网站公司负责修图吗
  • 专业网站建设推荐q479185700顶上用jsp源码做网站
  • Why框架是真正AGI的确定性证明
  • 主机服务器网站 怎么做网站建设模板个人
  • 发展速度迅猛 具有丰富的网站建设经验易语言怎么做无限打开网站
  • 怎么建立手机网站网站建设带主机
  • 想让网站的文章都被收录怎么做搜索引擎入口官网
  • 10大排序算法C#实现
  • 网络流 最大流
  • 集训总结(一)
  • 网址与网站的区别网站推广软文
  • 密云成都网站建设wordpress主动推送所有网址插件
  • 电子商务网站建设问题旅游网站国内外研究现状
  • 网站上常用字体网站建设的工作职责
  • 接订单去哪个网站计算网站制作教程
  • 亚马逊电子商务网站的建设网站建设素材使用应该注意什么
  • 互助盘网站开发沈阳市城市建设网站
  • seo优化网站的注意事项乐清网站网站建设
  • 天河网站 建设信科网络谷歌竞价广告
  • 美剧网站怎么做网站后台作用
  • 专业建站方案泗阳县住房和城乡建设局网站