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

HTML5 Canvas CurvyTip

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.98/5 (38投票s)

2013年2月24日

MIT

3分钟阅读

viewsIcon

87673

downloadIcon

870

CurvyTip HTML5 Canvas 实验。

引言

这是一份快速指南,介绍如何仅使用Canvas元素构建一个带有选择选项的弯曲形状的工具提示。 其外观在其他同类小部件中独树一帜。

背景

要继续学习本教程,您需要对 HTML5/Canvas 和 JavaScript 技术有所了解。

该实现的现场示例可以在这里找到:演示

开始

首先,我们需要创建一个 Canvas 元素,我们将在其上进行绘制

var element = document.createElement("canvas");

然后我们将 canvas 追加到 body 中

var tbody = document.body;
tbody.appendChild(element); 

在 Canvas 上绘制

  • 我们基本上做的是绘制三个弯曲的形状(我们可以绘制更多,但我喜欢三个),为了绘制每一个,我提出了自己的函数,它计算了绘制线条的不同路径点,然后通过使用 quadraticCurveTo 方法绘制形状(请注意,这可能会很容易改进,但我必须诚实地说,我从来没有在数学/几何方面做得那么好
  • function drawSegment(instance, segmentIndex) {
        var radius = instance.radius;
        var arcRadius = instance.arcRadius;
        var startAngle = GetAngle(instance, segmentIndex, 1);
        var endAngle = GetAngle(instance, segmentIndex, 0);
        var xCenter = instance.cX;
        var yCenter = instance.cY / .5
        var fol = parseInt(instance.segments / 2) == segmentIndex;
        var context = instance.context;
        var middleAngle = (startAngle + endAngle) / 2;
        var outterSX = getX(startAngle, radius, xCenter);
        var outterSY = getY(startAngle, radius, yCenter);
        var innerSX = getX(startAngle, radius - arcRadius, xCenter);
        var innerSY = getY(startAngle, radius - arcRadius, yCenter);
        var outterEX = getX(endAngle, radius, xCenter);
        var outterEY = getY(endAngle, radius, yCenter);
        var innerEX = getX(endAngle, radius - arcRadius, xCenter);
        var innerEY = getY(endAngle, radius - arcRadius, yCenter);
     
        var outterMX = getX(middleAngle, radius + instance.ctxFactor , xCenter);
        var outterMY = getY(middleAngle, radius + instance.ctxFactor , yCenter);
        var innerMX = getX(middleAngle, radius - arcRadius + instance.ctxFactor, xCenter);
        var innerMBX = getX(middleAngle + 4, radius - arcRadius, xCenter);
        var innerMPX = getX(middleAngle - 4, radius - arcRadius, xCenter);
        var innerMBY = getY(middleAngle + 4, radius - arcRadius, yCenter);
        var innerMPY = getY(middleAngle - 4, radius - arcRadius, yCenter);
        var innerMCX = getX(middleAngle, radius - arcRadius + instance.ctxFactor, xCenter);
        var innerMCY = getY(middleAngle, radius - arcRadius + instance.ctxFactor, yCenter + 8);
        var innerMY = getY(middleAngle, radius - arcRadius + instance.ctxFactor, yCenter);
     
        context.moveTo(outterSX, outterSY);
        context.quadraticCurveTo(outterSX, outterSY, innerSX, innerSY);
        context.moveTo(innerSX, innerSY);
     
        context.quadraticCurveTo(innerMX, innerMY, innerEX, innerEY);  
        context.quadraticCurveTo(innerEX, innerEY, outterEX, outterEY);
        context.quadraticCurveTo(outterMX, outterMY, outterSX, outterSY);
    };

    这个函数背后的想法是计算将根据角度坐标定义切片/形状的路径点。

    这是在 canvas 上一起绘制这三个形状后的效果

  • 现在我们在三个形状的中间绘制提示
  • if (fol) {
        context.quadraticCurveTo(innerSX, innerSY, innerMPX, innerMPY);
        context.lineTo(innerMCX, innerMCY);
        context.lineTo(innerMBX, innerMBY);
        context.quadraticCurveTo(innerEX, innerEY, innerEX, innerEY);
    } 

    这是在 canvas 上绘制后的效果。

  • 现在我们给 Canvas 加上一些颜色,我喜欢用渐变风格的背景而不是纯色。 为了实现这种外观,我们需要在绘制形状(drawSegment)之前指定渐变颜色。
  • var gradient = this.context.createLinearGradient(0, 0, 0, this.cY / .75);
    gradient.addColorStop(0, "rgb(255, 255, 255)");
    gradient.addColorStop(1, this.backColor);
    this.context.lineWidth = .5;
    this.context.fillStyle = gradient;
    this.context.strokeStyle = "black"; 

  • 然后我们添加一个阴影效果,为了在我们的表面上绘制一个阴影,我们将单独重绘相同的形状,这是因为我们不想给我们的原始形状一个阴影,因为我们的表面不是完全实心的(形状是分开的),并且阴影会在切片之间反射
  • if (this.hasShadow) {
        this.context.save();
        this.context.beginPath();
        this.context.shadowColor = this.shadowColor;
        this.context.shadowBlur = 5;
        this.context.shadowOffsetX = 0;
        this.context.shadowOffsetY = 5;
        for (var i = 0; i < this.segments; i++) {
            drawSegment(this, i);
        }
        this.context.closePath();
        this.context.fill();
        this.context.restore();
    }

  • 为了绘制每个部分的图像,我们使用 drawImage 函数,但为其添加阴影,以便给它一个非常小的浮雕效果,为了实现这一点,我们需要根据形状的角度位置计算图像的位置。
  • for (var i = 0; i < this.segments; i++) {
        var stAngle = GetAngle(this, i, 1);
        var edAngle = GetAngle(this, i, 0);
        var xCenter = this.cX;
        var yCenter = this.cY / .5
        var middleAngle = (stAngle + edAngle) / 2;
        var outterMX = getX(middleAngle, this.radius - (this.arcRadius / 2), xCenter);
        var outterMY = getY(middleAngle, this.radius - (this.arcRadius / 2), yCenter);
        if (typeof this.tiles[i] != 'undefined') {
            this.context.save();
            this.context.shadowColor = 'rgb(0, 0, 0)';
            this.context.shadowOffsetX = 0;
            this.context.shadowOffsetY = -1;
            this.context.shadowBlur = 1;
            //Keep image position
            this.context.translate(outterMX, outterMY)
            this.context.rotate(-this.rotation * TO_RADIANS);
            this.context.drawImage(this.tiles[i], -(this.tiles[i].width / 2), -(this.tiles[i].height / 2));
            this.context.restore();
        }
    } 
  • 这是最终结果应该显示的样子(我们的 CurvyTip)。

位置操作

  • 旋转:旋转 CurvyTip 实际上非常简单,我们需要做的就是旋转 canvas 本身。 为此,我们首先将我们的上下文转换为中心,然后按如下方式旋转:
  • this.clearCvs();
    // Move registration point to the center of the canvas
    this.context.save();
    this.context.translate(this.cX, this.cY);
    // Rotate  
    this.context.rotate(this.rotation * TO_RADIANS);
    // Move registration point back to the top left corner of canvas
    this.context.translate(-this.cX, -this.cY);
    this.context.scale(this.scale, this.scale);
    this.drawCanvas();
    this.context.restore();

    但我们也希望图像也旋转,否则当 canvas 旋转时图像将被旋转, 如图所示

    为了避免这种情况,我们需要在绘制图像时旋转它(在 canvas 旋转之前)

    if (typeof this.tiles[i] != 'undefined') {
        this.context.save();
        this.context.shadowColor = 'rgb(0, 0, 0)';
        this.context.shadowOffsetX = 0;
        this.context.shadowOffsetY = -1;
        this.context.shadowBlur = 1;
    
        //Keep image position
        this.context.translate(outterMX, outterMY)
        this.context.rotate(-this.rotation * TO_RADIANS);
        this.context.drawImage(this.tiles[i], -(this.tiles[i].width / 2), -(this.tiles[i].height / 2));
        this.context.restore();
    } 

  • 锚点:我们还可以将我们的 canvas 放置在我们的触发元素(通常是一个 div)的右/左/上方/下方,我们通过计算我们的触发元素(目标)的位置并识别它在屏幕上的偏移位置来做到这一点。
  • function SetPosition(instance) {
     
        var position = instance.anchor;
        var padding = 10;
        var offset = getOffset(instance.target);
        var twidth = instance.target.offsetWidth;
        var theight = instance.target.offsetHeight;
        var fol = parseInt(instance.segments / 2);
        var startAngle = GetAngle(instance, fol, 1);
        var endAngle = GetAngle(instance, fol, 0);
        var middleAngle = (startAngle + endAngle) / 2;
     
        var yCenter = instance.cY / .5;
        var xCenter = instance.cX;
        var innerMCY = getY(middleAngle, instance.radius - instance.arcRadius + instance.ctxFactor, yCenter + 8);
     
        var left = 0;
        var top = 0;
     
        switch (position) {
            case "top":
            case null:
            default:
                left = (offset.left - (instance.canvas.width / 2)) + (instance.target.offsetWidth / 2);
                top = offset.top - innerMCY - padding;
                break;
            case "bottom":
                left = (offset.left - (instance.canvas.width / 2)) + (instance.target.offsetWidth / 2);
                top = offset.top + theight - (instance.canvas.height - innerMCY) + padding;
                break;
            case "left":
                left = offset.left - padding - innerMCY;
                top = offset.top + (theight / 2) - innerMCY;
                break;
            case "right":
                left = offset.left + twidth - (instance.canvas.width - innerMCY) + padding;
                top = offset.top + (theight / 2) - innerMCY;
                break;
        }
    }

鼠标交互

即使 canvas 本身没有严格的方式来监听鼠标事件,也有几种方法可以实现这一点。

基本上,我们将创建一个引用每个区域位置的区域,然后使用 isPointInPath 函数将鼠标坐标与区域区域进行比较。

  • 这是一个非常粗略的例子,说明了如何实现它
  • var pos = this.mousePos;
    if (pos != null && this.context.isPointInPath(pos.x, pos.y)) {
        // handle onclick
        if (this.mouseClick && this.currentRegion.onclick !== undefined) {
          //mouse click
        }
        // handle onmouseover
        else if (!this.mouseOver && this.currentRegion.onmouseover !== undefined) {
            //mouse over
        }
    }
    else {
        if (this.currentRegion.onmouseout !== undefined) {
        //mouse out
        }
    }
  • 能够捕获鼠标事件可以让我们与 canvas 上的每个部分交互,从而可以进行悬停事件,如图所示
  • if (this.hitSegment == i) {
        this.context.save();
        this.context.beginPath();
        drawSegment(this, i);
        this.context.fillStyle = this.mouseOverColor;
        this.context.fill();
        this.context.closePath();
        this.context.restore();
    } 

如何使用它

初始化 curvyTip

var opts = {
  alwaysVisible: true, // Always keep canvas visible
  images: ['t.png', 'f.png', 'r.png'] // The images path
};

var curvyTip = new CurvyTip('targetElementId'); // Create canvas element
curvyTip.setOptions(opts); // Specify initial settings 

捕获事件

var curvyTip = new CurvyTip('targetElementId'); // Create canvas element
            
//Adding Listeners for supported Events.

curvyTip.addListener("click", function (index) {
      //handle event here
});
curvyTip.addListener("mouseover", function (index) {
      //handle event here
});
curvyTip.addListener("mouseout", function (index) {
      //handle event here
});

//Removing Listeners 

curvyTip.removeListener("click");
curvyTip.removeListener("mouseover");
curvyTip.removeListener("mouseout"); 

摘要

我已经谈论了 CurvyTip(HTML5 Canvas 实验)的一些细节,希望您喜欢这篇文章。

变更

版本 1.1 (2013 年 3 月 18 日)

  • OnLoad 位置修复,当 CurvyTip 在加载时显示时存在问题。
  • 稍微重构了代码,使其更灵活。 现在公开了 Gradient 属性,并且可用于覆盖默认的渐变样式。
© . All rights reserved.