如何在画布中的非alpha像素周围手动添加边框时提高速度

Jon*_*com 1 javascript canvas

我写了一个脚本,它采用像这样的图像(通常黑色是alpha):

字形

...并添加您喜欢的任何颜色的边框:

带边框的字体

然而,它不是很快.创建边框图层需要大约130毫秒作为这种小字体的画布.更大的字体需要更长的时间!

逻辑很简单:

/* This is more or less psuedo-code. */

// Blank image data where I will put the border.
var newData = newContext.getImageData(0, 0, canvas.width, canvas.height);

// The image I will be analyzing.
var oldData = oldContext.getImageData(0, 0, this.data.width, this.data.height);

// Loop through every pixel in oldData and remember where non-alpha pixels are.
var fontPixels = this._getNonAlphaPixels(oldData);

// Loop through relevant pixels, remember neighboring pixels, and add border.
for (var px in fontPixels) {
    for (var py in fontPixels[px]) {

        var borderPixels = this._getBorderPixels(px, py);
        for (var bx in borderPixels) {
            for (var by in borderPixels[bx]) {

                if (typeof fontPixels[bx] !== 'undefined' && 
                    typeof fontPixels[bx][by] !== 'undefined') 
                {
                    continue; // Do not draw borders inside of font.
                }

                newData.data[((newData.width * by) + bx) * 4] = color.red;
                newData.data[((newData.width * by) + bx) * 4 + 1] = color.green;
                newData.data[((newData.width * by) + bx) * 4 + 2] = color.blue;
                newData.data[((newData.width * by) + bx) * 4 + 3] = 255; //alpha
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

基本上我想知道:有人知道一种不需要逐像素操作的替代方法吗?或者是否可以对上述逻辑进行重大优化?

我应该提到,_getNonAlphaPixels这个时间可以忽略不计.并且_getBorderPixels执行时间仅占总时间的17%.

编辑

以下选择的答案非常有用.我的解决方案与下面的解决方案之间唯一的显着区别是,无论何时绘制文本,我都会绘制图像(字体).

谢谢肯.

小智 5

你可以用几种方式做到这一点.

技术1

一种是使用内置strokeText函数绘制文本轮廓.设置lineWidth将决定边框的厚度.但是,结果并不总是令人满意:

ctx.strokeStyle = color;
ctx.font = font;
ctx.lineWidth = 2;
ctx.strokeText(txt, x, y);
Run Code Online (Sandbox Code Playgroud)

结果是:

演示1

TEXT WITH BORDER DEMO 1

文本和画布目前在子像素级别上不那么准确,这与使用字体提示(或者更确切地说不使用),抗锯齿和其他方面有关.

技术2

无论如何,通过在"圆圈"中手动绘制文本来创建边框,您可以获得更好的结果:

var thick = 2;

ctx.fillStyle = color;
ctx.font = font;

ctx.fillText(txt, x - thick, y - thick);
ctx.fillText(txt, x, y - thick);
ctx.fillText(txt, x + thick, y - thick);
ctx.fillText(txt, x + thick, y);
ctx.fillText(txt, x + thick, y + thick);
ctx.fillText(txt, x, y + thick);
ctx.fillText(txt, x - thick, y + thick);
ctx.fillText(txt, x - thick, y);

ctx.fillStyle = '#fff';
ctx.fillText(txt, x, y);
Run Code Online (Sandbox Code Playgroud)

结果好多了,如下所示:

演示2

TEXT WITH BORDER DEMO 2

技术3

最后一种技术的缺点是我们要求画布在理论上将文本渲染9次 - 这是浪费时间......(见结果).

为了改善这一点,我们至少可以通过作为一次图像缓存边框文本,我们绘制文本的时间减为两个,并用它来绘制边框,然后画在上面的最后文本.

这里octx代表一个离屏画布上下文(c它自己的离屏画布),我们绘制我们将用于边框的文本.然后,我们更换圆形fillTextdrawImage.请注意,我们将基线设置为top以更容易控制文本最终的位置.

octx.textBaseline = ctx.textBaseline = 'top';
octx.fillStyle = color;
octx.font = ctx.font = font;
octx.fillText(txt, 0, 0);

ctx.drawImage(c, x - thick, y - thick);
ctx.drawImage(c, x, y - thick);
ctx.drawImage(c, x + thick, y - thick);
ctx.drawImage(c, x + thick, y);
ctx.drawImage(c, x + thick, y + thick);
ctx.drawImage(c, x, y + thick);
ctx.drawImage(c, x - thick, y + thick);
ctx.drawImage(c, x - thick, y);

ctx.fillStyle = '#fff';
ctx.fillText(txt, x, y);
Run Code Online (Sandbox Code Playgroud)

图像结果与上一个相同:

演示3

TEXT WITH BORDER DEMO 3

技术4

请注意,如果你想要更厚的边框,你可能会想要考虑实际进行圆形绘制 - 字面意思 - 使用cos/sin等.原因是因为在更高的偏移处,边界将开始分离:

在此输入图像描述

您可以改为使用Cos/Sin计算在文字圆圈中绘制文本,而不是添加一组绘制:

function drawBorderText(txt, x, y, font, color) {

    var thick = 7, 
        segments = 4,  /// number of segments to divide the circle in
        angle = 0,     /// start angle
        part,          /// degrees per segment, see below

        i = 0, d2r = Math.PI / 180;

    /// determine how many parts are needed. I just
    /// started with some numbers in this demo.. adjust as needed
    if (thick > 1) segments = 6;
    if (thick > 2) segments = 8;
    if (thick > 4) segments = 12;

    part = 360 / segments;

    ctx.fillStyle = color;
    ctx.font = font;

    /// draw the text in a circle
    for(;i < segments; i++) {
        ctx.fillText(txt, x + thick * Math.cos(angle * d2r),
                          y + thick * Math.sin(angle * d2r));
        angle += part;
    }

    ctx.fillStyle = '#fff';
    ctx.fillText(txt, x, y);
}
Run Code Online (Sandbox Code Playgroud)

请注意,在这种情况下,您可能需要绘制两个圆形,因为它们不具有实心的小点(例如,参见i上的点).

在此输入图像描述

这个演示中有点粗糙,但是举个例子.你可以通过为段设置不同的阈值来微调,也可以添加一个"内圆",其中文本包含这里的小细节(i).

TEXT WITH BORDER DEMO 4

结果

请注意,结果取决于各种因素:

  • 字体几何本身(包括字体提示).
  • 浏览器实现文本呈现及其优化
  • 中央处理器
  • 硬件加速

例如,如果没有硬件加速的Atom单核的计算机上,我得到两个演示2和3的Firefox(极光)为16ms(有时双的纯文字版).

在同一台计算机上的Chrome(Canary)中,基于文本的一个使用1-3毫秒,而缓存使用大约5毫秒.

在慢速计算机上,sin/cos方法大约需要8到11毫秒(几次达到5毫秒 - JSFiddle不是测试性能的最佳位置).

我目前无法访问其他硬件进行测试(这里的边距非常小,而且我不确定JavaScript能否接受它,我相信特别是Firefox的情况)但是与使用手动像素操作相比,在任何情况下,你都会有很大的增长.