PDF云注释背后的算法是什么?

8

我注意到几个PDF注释应用程序(Adobe Acrobat、Bluebeam等)都有一个算法,可以在多边形周围创建云朵图案:

Cloud Annotation in PDF

当您拖动此多边形的顶点时,云图案将重新计算:

Modified Cloud Annotation in PDF

请注意,弧线被重新计算以围绕多边形。它们没有被拉伸或扭曲。用于定义此功能的任何算法似乎都是行业标准。几个PDF编辑器允许您创建此内容,并且在每个编辑器中,当拖动顶点时,云弧看起来都是相同的。
我正在尝试创建一个WPF示例应用程序,以复制此功能,但似乎无法找到生成云图案的文档。
我非常熟练于图形设计和2D编程,并且能够创建拖动顶点的工具,但我需要帮助解决如何绘制这些弧线的问题。它看起来像是PathGeometry中的一系列ArcSegments
因此,我的问题是,创建这些弧线围绕多边形的算法是什么?
或者
在哪里可以找到这些行业标准的PDF图案、图纸和/或注释的文档?(云、箭头、边框等)
1个回答

18

您的草图中的云朵只是沿着每个多边形边缘绘制一系列具有一定重叠的圆。

绘制填充基本云形状的简单方法是先填充多边形,然后在填充的多边形上方绘制圆形。

当您想要使用部分透明颜色填充云朵时,这种方法就会失效,因为圆与彼此和基础多边形的重叠将被涂两次。它还会错过云曲线上的小卡通式超出部分。

绘制云朵的更好方法是首先创建所有圆,然后确定每个圆与其下一个邻居的相交角度。然后,您可以创建带有圆弧段的路径,您可以对其进行填充。轮廓由具有小偏移量的独立弧组成,用于结束角度。

在您的示例中,云弧之间的距离是静态的。通过使该距离变量并强制多边形边缘可被该距离均匀整除,很容易使多边形顶点处的弧重合。

以下是JavaScript的示例实现(不包括多边形拖动)。我不熟悉C#,但我认为基本算法很清楚。该代码是一个完整的网页,您可以保存并在支持画布的浏览器中显示;我已在Firefox中进行了测试。

绘制云的函数需要一个选项对象,如半径、弧距和以度为单位的过冲。我没有测试小多边形等退化情况,但在极端情况下,算法应该只为每个多边形顶点绘制单个弧线。
多边形必须按顺时针方向定义。否则,云会更像云层中的洞。如果周围的拐角弧线没有任何伪影,那将是一个很好的特性。
编辑:我提供了一个简单的在线测试页面来测试下面的云算法。该页面允许您玩转各种参数。它还很好地展示了算法的缺陷。(在FF和Chrome中测试过)
当起始和结束角度未正确确定时,就会出现伪影。对于非常钝角的情况,相邻拐角处的弧线之间也可能存在交点。我没有解决这个问题,但也没有太多考虑。
<!DOCTYPE html>

<html>
<head>
<meta charset="utf-8" />
<title>Cumulunimbus</title>
<script type="text/javascript">

    function Point(x, y) {
        this.x = x;
        this.y = y;
    }

    function get(obj, prop, fallback) {
        if (obj.hasOwnProperty(prop)) return obj[prop];
        return fallback;
    }

    /*
     *      Global intersection angles of two circles of the same radius
     */
    function intersect(p, q, r) {
        var dx = q.x - p.x;
        var dy = q.y - p.y;

        var len = Math.sqrt(dx*dx + dy*dy);
        var a = 0.5 * len / r;

        if (a < -1) a = -1;
        if (a > 1) a = 1;

        var phi = Math.atan2(dy, dx);
        var gamma = Math.acos(a);

        return [phi - gamma, Math.PI + phi + gamma];
    }

    /*
     *      Draw a cloud with the given options to the given context
     */
    function cloud(cx, poly, opt) {        
        var radius = get(opt, "radius", 20);
        var overlap = get(opt, "overlap", 5/6);
        var stretch = get(opt, "stretch", true);



        // Create a list of circles

        var circle = [];        
        var delta = 2 * radius * overlap;

        var prev = poly[poly.length - 1];
        for (var i = 0; i < poly.length; i++) {
            var curr = poly[i];

            var dx = curr.x - prev.x;
            var dy = curr.y - prev.y;

            var len = Math.sqrt(dx*dx + dy*dy);

            dx = dx / len;
            dy = dy / len;

            var d = delta;

            if (stretch) {
                var n = (len / delta + 0.5) | 0;

                if (n < 1) n = 1;
                d = len / n;
            }

            for (var a = 0; a + 0.1 * d < len; a += d) {
                circle.push({
                    x: prev.x + a * dx,
                    y: prev.y + a * dy,
                });
            }

            prev = curr;
        }



        // Determine intersection angles of circles

        var prev = circle[circle.length - 1];
        for (var i = 0; i < circle.length; i++) {
            var curr = circle[i];
            var angle = intersect(prev, curr, radius);

            prev.end = angle[0];
            curr.begin = angle[1];

            prev = curr;
        }



        // Draw the cloud

        cx.save();

        if (get(opt, "fill", false)) {
            cx.fillStyle = opt.fill;

            cx.beginPath();
            for (var i = 0; i < circle.length; i++) {
                var curr = circle[i];

                cx.arc(curr.x, curr.y, radius, curr.begin, curr.end);
            }
            cx.fill();
        }

        if (get(opt, "outline", false)) {
            cx.strokeStyle = opt.outline;
            cx.lineWidth = get(opt, "width", 1.0);

            var incise = Math.PI * get(opt, "incise", 15) / 180;

            for (var i = 0; i < circle.length; i++) {
                var curr = circle[i];

                cx.beginPath();
                cx.arc(curr.x, curr.y, radius,
                    curr.begin, curr.end + incise);
                cx.stroke();
            }
        }

        cx.restore();
    }

    var poly = [
        new Point(250, 50),
        new Point(450, 150),
        new Point(350, 450),
        new Point(50, 300),
    ];

    window.onload = function() {
        cv = document.getElementById("cv");
        cx = cv.getContext("2d");

        cloud(cx, poly, {
            fill: "lightblue",        // fill colour
            outline: "black",         // outline colour
            incise: 15,               // overshoot in degrees
            radius: 20,               // arc radius
            overlap: 0.8333,          // arc distance relative to radius
            stretch: false,           // should corner arcs coincide?
        });
    }

</script>
</head>

<body>
<canvas width="500" height="500" id="cv"></canvas>
</body>

</html>

2
这绝对是我在StackOverflow上收到的最佳回答。我希望我能为您这么不可思议的努力提供更多积分。您的代码结构良好,易于理解,并且无可否认地令人印象深刻!谢谢您,M Oehm。 - Laith
然而,有一个小问题。如果形成正方形的点从右下角、右上角、左上角和左下角开始,则绘制的云朵是相反的。 - chitgoks
@chitgoks:错误在于文章中,而不是代码中。必须定义多边形的方向,因为它用于确定云的“外部”。文章中说顺时针,但是在HTML画布中从上到下的y轴是逆时针的。因此交换第二个和第四个顶点,你就应该没问题了。 - M Oehm
@m oehm 对的,我会检查一下。因为我不是一直在处理四边形多边形,所以我会看看能否成功更改您现有的代码使其正常工作。如果涉及许多顶点,我不确定需要交换哪些点的公式是什么。 - chitgoks
1
嗯,@chitgoks,如果云朵被画反了,请反转您的顶点列表。 - M Oehm

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