65.9K
CodeProject 正在变化。 阅读更多。
Home

HTML 5 Canvas - 一个简单的绘图程序

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.83/5 (5投票s)

2012年3月21日

CPOL

5分钟阅读

viewsIcon

55263

downloadIcon

1275

深入探索 HTML5 的炫酷 Canvas 对象。

引言

这是一篇关于 HTML5 Canvas 的简单有趣的介绍,以及用户如何与之交互。

代码

背景 

HTML5 为浏览器带来了全新的、令人兴奋的功能集。支持 HTML5 的最新一代浏览器支持 canvas 对象。通过一些简单的 JavaScript,您可以让访问您网站的用户能够修改图像。

本项目旨在作为一个简单的概念验证,允许将图像加载到 canvas 中并由用户修改。通过一些简单的扩展,可以将其扩展到将新图像保存到服务器或用户的本地计算机。

使用代码

整个项目是用客户端 JavaScript 编写的,并使用了一些 jQuery 来提供一些快捷方式。如果您不熟悉 jQuery,可以很容易地用经典的 JavaScript 替换它。

这里有一个可用的工作示例 here。您的浏览器必须支持 HTML5 才能正常工作(已在 IE9+ 和 FireFox 10+ 上测试)。否则,您将看到奇怪(或没有任何)的结果。

从一个空白的 HTML 页面开始。如果您使用 jQuery,请记住包含它。

 我们将从一个 CanvasManager 对象开始。这个 JavaScript 类封装了 canvas 及其辅助方法。  如果一下子需要消化这么多代码,请不要担心。我将在下面详细介绍。

//
// The CanvasManager class
//
      
     function CanvasManager(canvasId) {
            this.canvas = $('#' + canvasId);
            this.imgColor = [255, 255, 255];
            this.buttonDown = false;
            this.lastPos = [0, 0];
            this.mode;

            this.canvas.mouseup(function () {
                canvasMgr.buttonDown = false;
            });

            this.canvas.mouseout(function () {
                canvasMgr.buttonDown = false;
            });

            this.canvas.mousedown(function () {
                if (!canvasMgr.mode)
                    return;

                if (arguments.length > 0) {
                    var arg = arguments[0];
                    var ctx = canvasMgr.canvas.get(0).getContext("2d");
                    var offsets = getOffsets(arg);

                    if (canvasMgr.mode == 'dropper') {                        
                        var imgData = ctx.getImageData(offsets.offsetX, offsets.offsetY, 1, 1);

                        canvasMgr.imgColor[0] = imgData.data[0];
                        canvasMgr.imgColor[1] = imgData.data[1];
                        canvasMgr.imgColor[2] = imgData.data[2];

                        $('#dvSwatch').css('background-color', 'rgb(' + imgData.data[0] +
                            ',' + imgData.data[1] +
                            ',' + imgData.data[2] + ')');
                    }
                    else if (canvasMgr.mode == 'paint') {
                        canvasMgr.buttonDown = true;
                    }
                    else if (canvasMgr.mode == 'text') {
                        canvasMgr.lastPos[0] = offsets.offsetX;
                        canvasMgr.lastPos[1] = offsets.offsetY;
                    }
                }
            });

            this.canvas.mousemove(function (e) {
                var arg = e;
                var ctx = canvasMgr.canvas.get(0).getContext("2d");
                if (canvasMgr.mode == 'paint') {
                    if (canvasMgr.buttonDown) { //mousebutton down

                        var offsets = getOffsets(e);
                        var imgData = ctx.getImageData(offsets.offsetX - 5, offsets.offsetY - 5, 10, 10);

                        for (i = 0; i < imgData.width * imgData.height * 4; i += 4) {
                            imgData.data[i] = canvasMgr.imgColor[0];
                            imgData.data[i + 1] = canvasMgr.imgColor[1];
                            imgData.data[i + 2] = canvasMgr.imgColor[2];
                            imgData.data[i + 3] = 255;
                        }

                        ctx.putImageData(imgData, offsets.offsetX - 5, offsets.offsetY - 5);
                    }
                }
            });

            $(window).keypress(function (e) {
                if (canvasMgr.mode == 'text') {
                    var canvasDOM = canvasMgr.canvas.get(0);
                    var ctx = canvasDOM.getContext("2d");
                    ctx.font = "20pt Arial";
                    ctx.textBaseline = "bottom";
                    ctx.textAlign = "left";
                    ctx.fillStyle = 'rgb(' + canvasMgr.imgColor[0] +
                            ',' + canvasMgr.imgColor[1] +
                            ',' + canvasMgr.imgColor[2] + ')';

                    ctx.fillText(String.fromCharCode(e.which), canvasMgr.lastPos[0], canvasMgr.lastPos[1]);
                    var textMTX = ctx.measureText(String.fromCharCode(e.which));
                    canvasMgr.lastPos[0] += textMTX.width + 1;
                }
            });
        }

        CanvasManager.prototype.setMode = function (mode) {
            this.mode = mode;
            var cur;
            switch (this.mode) {
                case 'text':
                    cur = 'text';
                    break;
                case 'dropper':
                    cur = 'pointer'
                    break;
                case 'paint':
                    cur = 'crosshair';
                    break;
                default:
                    cur = 'default';
                    break;

            }
            var localCanvas = this.canvas;
            
            this.canvas.mouseover(function () {
                localCanvas.css('cursor', cur);
            });
        }

        CanvasManager.prototype.drawImage = function (image, x, y, w, h) {
            var canvas = this.canvas.get(0);
            var ctx = canvas.getContext("2d");
            ctx.drawImage(image, x, y, w, h);
        }

该类支持绘图、设置颜色和文本。其余部分封装了事件和 canvas 辅助功能。

让我们看一下构造函数

    
function CanvasManager(canvasId) {
     this.canvas = $('#' + canvasId); 
     this.imgColor = [255, 255, 255];
     this.buttonDown = false;
     this.lastPos = [0, 0];
     this.mode = '';
     ...

canvas 成员实际上是 DOM 对象的 jQuery 包装器。

imgColor 成员存储用户选择的颜色。默认颜色是白色(#FFFFFF)。

为了绘图,我需要记住鼠标按钮的状态(按下或未按下)。buttonDown 布尔成员负责处理此问题。

lastPos 成员记住最后一次鼠标位置。它用于绘制文本。

最后但同样重要的是,mode 成员表示 canvas 的模式(绘画、设置颜色或文本)。

接下来在构造函数中,我们为 canvas 分配事件处理程序。我们将使用 mousemove 事件,因为它最复杂。

this.canvas.mousemove(function (e) {
    var arg = e;
    var ctx = canvasMgr.canvas.get(0).getContext("2d");
    if (canvasMgr.mode == 'paint') {
        if (canvasMgr.buttonDown) { //mousebutton down

            var offsets = getOffsets(e);
            var imgData = ctx.getImageData(offsets.offsetX - 5, offsets.offsetY - 5, 10, 10);

            for (i = 0; i < imgData.width * imgData.height * 4; i += 4) {
                imgData.data[i] = canvasMgr.imgColor[0];
                imgData.data[i + 1] = canvasMgr.imgColor[1];
                imgData.data[i + 2] = canvasMgr.imgColor[2];
                imgData.data[i + 3] = 255;
            }

            ctx.putImageData(imgData, offsets.offsetX - 5, offsets.offsetY - 5);
        }
    }
});

关于在 canvas 上绘图,您应该注意以下几点:

我们引用 canvasManager 的 canvas 对象,并使用 jQuery 的 get() 方法来获取 DOM canvas 对象(而不是 jQuery 包装器)。 

我们检查 canvas 的模式。如果模式设置为“paint”,我们继续。

接下来,我们检查鼠标按钮状态是否为“down”。这在 mousedown 事件处理程序中设置。然后,我们就可以开始绘图了。我有一个名为 getOffsets() 的辅助函数来获取鼠标位置。我们需要一个辅助函数,因为 IE 使用 OffsetX/OffsetY,而 Mozilla 使用 LayerX/LayerY。该方法在本篇文章的底部显示。

要绘制 canvas 的一部分,您需要获取要绘制的 canvas 的块,然后对其进行更改。Context.getImageData() 方法从绘图区域返回像素数组。

每个像素占用数组的四个元素;红色=0,绿色=1,蓝色=2,Alpha=3。所以我们以四个元素为步长循环遍历数组。对于每个像素,我从 CanvasManager 的 imgColor 成员设置颜色属性。最后,在 Context.putImageData() 方法中将您的 canvas 块写回主 canvas。

在 canvas 上绘制文本  

     
$(window).keypress(function (e) {
          if (canvasMgr.mode == 'text') {
               var canvasDOM = canvasMgr.canvas.get(0);
               var ctx = canvasDOM.getContext("2d");
               ctx.font = "20pt Arial";
               ctx.textBaseline = "bottom";
               ctx.textAlign = "left";
               ctx.fillStyle = 'rgb(' + canvasMgr.imgColor[0] +
                            ',' + canvasMgr.imgColor[1] +
                            ',' + canvasMgr.imgColor[2] + ')';

               ctx.fillText(String.fromCharCode(e.which), canvasMgr.lastPos[0], canvasMgr.lastPos[1]);
               var textMTX = ctx.measureText(String.fromCharCode(e.which));
               canvasMgr.lastPos[0] += textMTX.width + 1;
           }
       });

在 canvas 上绘制文本比较棘手,因为 canvas 对象似乎不接受 keypress 或 keydown 事件。我们需要为此挂钩 DOM window 对象。

再次,我们获取 canvas 的 Context 对象以在 canvas 上执行任何图形操作。

请注意,我正在使用 CanvasManager 的模式属性来确保我们处于“text”模式。

Context 对象支持以下用于渲染文本的属性:font、textBaseline、textAlign 和 fillStyle。使用基本的 CSS 语法来设置这些属性。 

要绘制文本,只需调用 Context.fillText() 方法。fillText() 调用提供转换为字符串的 ASCII 代码(String.fromCharCode(e.which))以及来自 CanvasManager 的 lastPos 值的 X 和 Y。

为了设置下一个字符的位置,使用 Context.MeasureText() 来获取前一个字符的长度,并增加 lastPos 值以供下一个字符使用。

由于跨浏览器不兼容,我不得不编写一个实用函数来返回鼠标位置的 X 和 Y 坐标。这是代码。请注意,我在 Firefox 上看到了一些奇怪的行为。IE 和 Chrome 都做得很好。 

function getOffsets(evt) {
    var offsetX, offsetY;
    if (typeof evt.offsetX != 'undefined') {
        offsetX = evt.offsetX;
        offsetY = evt.offsetY;
    }
    else if (typeof evt.clientX != 'undefined') {
        offsetX = evt.clientX + document.body.scrollLeft + document.documentElement.scrollLeft;
        offsetY = evt.clientY + document.body.scrollTop + document.documentElement.scrollTop;
        offsetX -= canvasMgr.canvas.get(0).offsetLeft;
        offsetY -= canvasMgr.canvas.get(0).offsetTop;
    }
    return { 'offsetX': offsetX, 'offsetY': offsetY };
}

最后,将 canvas 连接到控件的代码很简单:设置 CanvasManager 模式的按钮。

<input type="button" id="btnText" value="Text" onclick="canvasMgr.setMode('text');" /> <input type="button" id="btnPaint" value="Paint" onclick="canvasMgr.setMode('paint');" /> <input type="button" id="btnDropper" value="Set Color" onclick="canvasMgr.setMode('dropper');" />
    

兴趣点 

  • canvas 对象偏好 PNG 图像文件格式。我相信这样想是因为 PNG 是未来的趋势。它支持 JPEG,但只是为了迎合你们这些老派开发者。
  • 在这个示例中,canvas 是 500X500px。您的图像应具有相同的尺寸。
  • 如果您是从 Windows 世界学习绘图的,您会发现 HTML5 canvas 非常容易上手。您可能会使用 Win32 绘图 API 参考来掌握 canvas 的基础知识。Context 对象与 Graphics 对象是表兄弟关系。
  • 正如任何 Web 开发人员很快就会发现的那样,浏览器之间的差异会导致头痛、过早脱发等。寻找鼠标坐标花了一些时间。请记住,互联网已经为您找到了解决方案。Bing 是您的朋友。 
  • jQuery 是对 Web 开发来说最棒的事情。jQuery 最好的、但又被低估的好处之一是,有人已经解决了跨浏览器问题,并将其封装在一个友好的 API 下。
  • keypress 事件应包含过滤掉不可打印字符的代码。Enter 和 Backspace 不会渲染文本,而是让浏览器自行处理。
  • 如果您觉得这篇文章很有趣,请发表评论。这将有助于我脆弱的自尊心。

历史 

最初发布日期 2012/3/20  

更新于 2012/4/1,以修正 Firefox 的坐标查找问题。

© . All rights reserved.