【问题标题】:Writing a paint program à la MS Paint - how to interpolate between mouse move events?编写一个绘图程序 à la MS Paint - 如何在鼠标移动事件之间进行插值?
【发布时间】:2011-03-21 19:42:51
【问题描述】:

我想写一个MS Paint风格的绘画程序。

为了在用户移动鼠标时在屏幕上绘制东西,我必须等待鼠标移动事件并在收到鼠标移动事件时在屏幕上绘制。显然,mose 移动事件并不经常发送,所以我必须通过在当前鼠标位置和前一个鼠标位置之间画一条线来插入鼠标移动。在伪代码中,这看起来像这样:

var positionOld = null

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

现在我的问题:用直线段插值对我来说看起来太锯齿了,你能推荐一个更好的插值方法吗? GIMP 或 Adob​​e Photoshop 是用什么方法实现的?

或者,有没有办法增加我收到的鼠标移动事件的频率?我使用的 GUI 框架是wxWidgets

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

编辑:澄清:我想要看起来比直线段更平滑的东西,请看图片(原始尺寸):

EDIT2:我使用的代码如下所示:

-- 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 是我得到相当低频率的鼠标移动事件的原因。

【问题讨论】:

  • 我不知道这是否可能,但是您是否考虑过运行一个计时器来检查鼠标位置?然后你就可以控制消息的频率了。

标签: user-interface wxwidgets mouseover interpolation paint


【解决方案1】:

现场演示

The way to go is 

点的样条插值

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

我采用here 演示的解决方案并对其进行了修改。他们在您停止绘图后计算样条曲线。我修改了代码,使其立即绘制。您可能会看到样条曲线在绘图期间正在发生变化。对于实际应用,您可能需要两个画布 - 一个带有旧绘图,另一个仅带有当前绘图,它们会不断变化,直到您的鼠标停止。

版本 1 使用样条简化 - 删除靠近直线的点 - 这会产生更平滑的样条,但会产生不太“稳定”的结果。版本 2 使用线上的所有点并产生更稳定的解决方案(并且计算成本更低)。

【讨论】:

    【解决方案2】:

    我同意 harviz - 问题没有解决。它应该通过在优先级线程中记录鼠标移动来在操作系统级别解决,但我所知道的操作系统都没有这样做。但是,应用开发者也可以通过比线性更好的插值来解决这个操作系统限制。

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

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

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

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

    这意味着我们以与线插值相同的角度进入第二个点,但以与前一段进入的相同角度离开起点,使边缘平滑。我们使用两个控制点距离的线段长度来使弯曲的大小适合其比例。

    下图显示了数据点很少的结果(以灰色表示)。

    序列从左上角开始,到中间结束。

    这里仍然存在一定程度的不安,如果同时使用前一个点和下一个点来调整两个角度,这可能会减轻,但这也意味着画的点比得到的少一个点。我觉得这个结果已经令人满意,所以我没有尝试。

    【讨论】:

      【解决方案3】:

      所以,当我看到手绘曲线的锯齿边缘的问题时,当鼠标移动得非常快时,并没有解决!!!在我看来,需要解决系统中mousemove事件的轮询频率,即使用不同的鼠标驱动程序或smf ..第二种方法是数学..使用某种算法,准确弯曲之间的直线当鼠标事件被轮询出来时有两点。为了清晰的视图,您可以比较如何在 Photoshop 中绘制徒手线以及在 mspaint 中如何绘制。谢谢大家.. ;)

      【讨论】:

        【解决方案4】:

        用线段插值鼠标移动很好,GIMP 也是这样做的,如下面的快速鼠标移动屏幕截图所示:

        因此,平滑度来自于鼠标移动事件的高频率。 WxWidgets 可以做到这一点,正如related question 的示例代码所示。

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

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

        【讨论】:

        • 您发布的代码与我的代码有何不同,只是没有在点之间进行插值?
        • 我在related question 中的代码可能与您的几乎相同。 (如果你把它贴在那里,我会接受你的)。此处问题中的我的 Haskell 代码有所不同,因为它会在每次鼠标移动时将 800x600 位图复制到屏幕上。
        【解决方案5】:

        您可以使用样条线使它们变得非常平滑: http://freespace.virgin.net/hugo.elias/graphics/x_bezier.htm

        但是您必须将每条线段的绘制延迟到一帧之后,这样您就有了起点和终点,以及可用于计算的下一个和上一个点。

        【讨论】:

        • 关于样条线的好链接,但它仅根据两个点和两个方向定义样条线。您是否有关于如何在样条曲线上给定三个或四个点来创建样条曲线段的详细信息?毕竟,我收到的只是鼠标位置列表。
        • 当然,您只需将每个点的方向定义为与前一个点和下一个点之间的线平行。对于序列中的第一个和最后一个点,只需将方向设置为零向量即可。
        • @HeinrichApfelmus 是对的,这不是一个好主意。如果没有一些巧妙的统计数据评估相邻点,您将无法弄清楚这些方向。
        • 啊哈,你从 "simply" 行调用派生方向 - 我想看看你的“简单”代码......无论如何,你有点忘记了这一行是你应该创造的,这不是你的输入:-)
        • 在哪里可以找到关于贝塞尔曲线的参考文章? (链接已失效。)
        【解决方案6】:

        我认为您需要查看 wxWidgets 的 Device Context 文档。

        我有一些这样绘制的代码:

        //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++,但你说这门语言无关紧要,所以我还是希望它有所帮助。

        这让我可以这样做:

        这似乎比您的示例更流畅。

        【讨论】:

        • 感谢您的帮助,但我想画一些比线条更平滑的东西。要么更频繁地出现鼠标移动事件。
        • 也许我理解错了;这很顺利。请参阅此图像:farm5.static.flickr.com/4108/4837817079_a0c191586d.jpg 示例(我使用了上面的代码)。你说的更平滑是什么意思?更宽且抗锯齿?
        • 啊,好吧,看起来很平滑,即线段不可见。看起来你得到了更多的鼠标移动事件,可能是因为我使用了不同的设置。编辑了我的问题。 wxStaticBitmap 的最大尺寸似乎为 64x64;您对屏幕区域还有其他建议吗?
        • 64x64 限制仅适用于 Windows 9x,我假设您没有针对它。 wxStaticBitmap 应该适用于几乎任何事情。但是,如果您想要一些真正有效的东西,我发现在 wxWidgets 中操作图像数据的最佳方法是存储一个字节数组(我在 c++ 中使用 unsigned char),其中每个字节是红色、绿色或一个点的蓝色值。然后你可以将它加载到 wxImage 并显示在你想要的任何容器中。我知道这不是一个很好的解释。如果你想走那条路,请告诉我,我会添加一个解释它的答案。
        • 我还没有对 wxWidgets 进行计时以查看我得到的性能,但我猜我比 50ms 更接近 20ms。问题可能是 wxWidgets-> Haskell 发生的一些瓶颈?除非您使用的是较旧的硬件,否则我想会是这种情况。
        猜你喜欢
        • 1970-01-01
        • 2019-05-11
        • 2010-11-23
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多