仿照MS Paint编写绘图程序-如何在鼠标移动事件之间插值?

12
我希望编写一个类似于MS Paint的绘图程序。
当用户移动鼠标时,我需要等待鼠标移动事件,并在接收到事件时在屏幕上进行绘制。显然,鼠标移动事件并不经常发送,因此我必须通过在当前鼠标位置和上一个位置之间绘制一条线来插值鼠标移动。 伪代码如下:
var positionOld = null

def handleMouseMove(positionNew):
    if mouse.button.down:
        if positionOld == null:
            positionOld = positionNew
        screen.draw.line(positionOld,positionNew)
        positionOld = positionNew

现在我的问题是:使用直线段进行插值看起来太过锯齿状,你能推荐一种更好的插值方法吗?GIMP或Adobe Photoshop使用哪种方法?

另外,有没有办法增加我接收到的鼠标移动事件的频率?我正在使用的GUI框架是wxWidgets

GUI框架:wxWidgets。
(编程语言为Haskell,但这里与本题无关)

编辑:澄清:我想要比直线段更平滑的东西,参见图片(原始大小):

jagged lines drawn between mouse positions

编辑2: 我正在使用的代码看起来像这样:

-- create bitmap and derive drawing context
im      <- imageCreateSized (sy 800 600)
bitmap  <- bitmapCreateFromImage im (-1)    -- wxBitmap
dc      <- memoryDCCreate                   -- wxMemoryDC
memoryDCSelectObject dc bitmap

...
-- handle mouse move
onMouse ... sw (MouseLeftDrag posNew _) = do
    ...
    line dc posOld posNew [color     := white
                          , penJoin  := JoinRound
                          , penWidth := 2]
    repaint sw                              -- a wxScrolledWindow

-- handle paint event
onPaint ... = do
    ...
    -- draw bitmap on the wxScrolledWindow
    drawBitmap dc_sw bitmap pointZero False []

这可能会产生影响。也许我的wx-classes选择是我得到相对较低的鼠标移动事件频率的原因。


2
我不知道这是否可能,但你考虑过运行一个检查鼠标位置的计时器吗?这样你就可以控制消息的频率。 - Frank Schwieterman
6个回答

4

在线演示

图片描述

The way to go is 

点的样条插值

解决方法是存储点的坐标,然后进行样条插值。

我使用了这里演示的解决方案并进行了修改。他们在绘制完成后才计算样条。我修改了代码以立即绘制。但你可能会看到,样条在绘制过程中会发生变化。对于实际应用,您可能需要两个画布 - 一个存储旧的绘图,另一个仅包含当前绘图,直到鼠标停止为止一直改变。

版本1使用样条简化 - 删除接近线的点 - 这样可以得到更平滑的样条,但产生较不稳定的结果。而版本2使用线上的所有点,可以得出更稳定的解决方案(且计算负担更小)。


2

1
这是一个关于样条曲线的不错的链接,但它只通过两点和两个方向来定义样条曲线。你有关于如何在给定样条曲线上的三或四个点时创建样条线段的详细信息吗?毕竟,我拿到的只是鼠标位置的列表。 - Heinrich Apfelmus
1
@HeinrichApfelmus是正确的,这不是一个好主意。如果没有一些聪明的统计学方法来评估相邻点,你将无法弄清楚这些方向。 - Tomas
1
啊哈,你称从线条中推导方向是“简单”的-那我想看看你的“简单”代码...无论如何,你有点忘记了线条是你要创建的,而不是你的输入 :-) - Tomas
@Tomas - 这些点是你的输入。两个点之间的直线很容易构造。对于点n的切线,简单地说就是与从点n-1到点n+1的直线平行的线。这是非常非常琐碎的。 - Rocketmagnet
1
在哪里可以找到有关贝塞尔曲线的文章?(链接已失效。) - john_who_is_doe
显示剩余2条评论

2

所以,我发现当鼠标移动得非常快时,手绘制曲线的锯齿状边缘问题仍未解决!!! 我认为需要通过在系统中使用不同的鼠标驱动程序或smf等来处理mousemove事件的轮询频率。第二种方法是数学..使用某种算法,在鼠标事件被轮询出时准确地弯曲两点之间的直线.. 为了更清楚地看到,您可以比较在Photoshop中如何绘制手绘线和在MSPaint中如何绘制手绘线.. 感谢大家.. ;)


1

我认为你需要查看 wxWidgets 的 设备上下文 文档。

我有一些绘制代码,像这样:

//screenArea is a wxStaticBitmap
int startx, starty;
void OnMouseDown(wxMouseEvent& event)
{
    screenArea->CaptureMouse();
    xstart = event.GetX();
    ystart = event.GetY();
    event.Skip();
}
void OnMouseMove(wxMouseEvent& event)
{
    if(event.Dragging() && event.LeftIsDown())
    {
        wxClientDC dc(screenArea);
        dc.SetPen(*wxBLACK_PEN);
        dc.DrawLine(startx, starty, event.GetX(), event.GetY());
    }
    startx = event.GetX();
    starty = event.GetY();
    event.Skip();
}

我知道这是C++,但你说语言无关,所以我希望它能帮到你。

这让我可以做到这个:

alt text

这似乎比你的例子要平滑得多。


谢谢你的帮助,但我想画一些比线条更平滑的东西。要么就让鼠标移动事件更频繁地出现。 - Heinrich Apfelmus
1
也许我误解了,这很流畅。请参考此图像:http://farm5.static.flickr.com/4108/4837817079_a0c191586d.jpg 作为示例(我使用了上面的代码)。您所说的更平滑是什么意思?更宽和抗锯齿? - Evan Cordell
啊,好的,看起来很流畅,也就是说线段不可见。似乎你得到了更多的鼠标移动事件,可能是因为我使用了不同的设置。我编辑了我的问题。看起来wxStaticBitmap有一个最大尺寸为64x64;你有其他屏幕区域的建议吗? - Heinrich Apfelmus
1
64x64的限制只适用于Windows 9x,我假设你不是在针对它。wxStaticBitmap对于几乎任何事情都应该是可以的。然而,如果你想要真正高效的东西,我发现在wxWidgets中操作图像数据的最佳方法是存储一个字节数组(我在c++中使用unsigned char),其中每个字节是一个点的红色、绿色或蓝色值。然后,你可以将其加载到wxImage中,并在任何容器中显示。我知道那不是一个很好的解释;如果你想走这条路,就让我知道,我会添加一个解释性答案。 - Evan Cordell
1
我还没有计时wxWidgets以查看我的性能,但我猜我会得到接近20ms而不是50ms。问题可能是在wxWidgets->Haskell之间发生的某些瓶颈?除非您使用的是旧硬件,否则我想那就是情况。 - Evan Cordell
显示剩余3条评论

1

我同意harviz的观点-问题没有得到解决。应该通过在优先级线程中记录鼠标移动来在操作系统层面上解决,但据我所知,没有一个操作系统这样做。然而,应用程序开发人员也可以通过比线性插值更好的插值方法绕过这个操作系统限制。

由于鼠标移动事件不总是足够快,线性插值并不总是足够好。

我对Rocketmagnet提出的样条曲线想法进行了一些实验。

不要将线条放在两个点A和D之间,而是查看前置点P,并使用具有以下控制点B = A + v'和C = D-w'的三次样条曲线。

v = A - P,
w = D - A,
w' = w / 4 and
v' = v * |w| / |v| / 4.

这意味着我们以与线性插值相同的角度落入第二个点,但以与前一段相同的角度从起始点出发,使边缘平滑。我们使用片段长度作为控制点距离的大小,以使弯曲的大小适合其比例。下图显示了非常少的数据点(用灰色表示)的结果。

enter image description here

序列从左上角开始,到中间结束。
这里仍然存在一定程度的不安,使用前一个和后一个点来调整两个角度可以减轻这种情况,但这也意味着要比所得到的少画一个点。我认为这个结果已经令人满意了,所以我没有尝试。

1
使用线段插值鼠标移动是可以的,GIMP也是这样做的,如下面这张截图所示,它显示了一个非常快的鼠标移动。

GIMP uses line segments, too

因此,平滑性来自于高频鼠标移动事件。WxWidgets可以做到这一点,正如相关问题的示例代码所示。

问题在于你的代码,Heinrich。也就是说,先将内容绘制到一个大型位图中,然后再将整个位图复制到屏幕上并不便宜!为了估计你需要多高效率,可以将问题与视频游戏进行比较:每秒30次鼠标移动事件对应30帧每秒的平滑速率。双缓冲区的复制对于现代机器来说并不是问题,但WxHaskell可能没有针对视频游戏进行优化,因此你会遇到一些抖动是不足为奇的。

解决方案是只绘制必要的部分,即仅绘制线条直接在屏幕上,例如如上链接所示。


你发的代码和我的有什么不同,除了不在点之间插值? - Evan Cordell
我的代码在相关问题中可能与你的几乎相同。(如果你在那里发布,我会接受你的代码)。我在这里的Haskell代码不同之处在于,它在每次鼠标移动时将800x600位图复制到屏幕上。 - Heinrich Apfelmus

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接