Click here to Skip to main content
Click here to Skip to main content
Add your own
alternative version

HTML5 Canvas CurvyTip

, 21 Mar 2013 MIT
CurvyTip HTML5 Canvas experiment.
CurvyTipSource.zip
/*
 * CurvyTip
 *
 * Mario F.
 * Licensed under the MIT.
 *
 * HTML5 Tooltip gadget that adds/extends selection options to html elements
 * Sourcecode created from scratch by Mario F. */

var TO_RADIANS = Math.PI / 180;

var CurvyTip = function (targetElement) {
        var img = new Image();
        var that = this;

        var element = document.createElement("canvas");
        element.setAttribute("class", 'tooltip'); //For Most Browsers
        element.setAttribute("className", 'tooltip'); //For IE; harmless to other browsers.

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

        this.canvas = element;
        this.canvas.width = 200;
        this.canvas.height = 200;
        this.context = this.canvas.getContext("2d");
        this.stage = undefined;
        this.listening = false;
        this.mousePos = null;
        this.mouseClick = false
        this.mouseOver = false;
        this.mouseMove = false;
        this.currentRegion = null;
        this.regionIndex = -1;
        this.lastRegionIndex = -1;
        this.mouseOverRegionIndex = -1;
        this.hasShadow = true;
        this.target = document.getElementById(targetElement);
        this.alwaysVisible = false;
        this.hitSegment = -1;
        this.startAngle = 130.0;
        this.endAngle = 50.0;
        this.segments = 3;
        this.radius = 150;
        this.arcRadius = 50;
        this.rotation = 0;
        this.scale = 1;
        this.tiles = [];
        this.images = [];
        this.currentRegionEvent = {};
        this.anchor = "top";
        this.isbeingClosed = false;
        this.shadowColor = 'rgba(0,0,0,0.6)';
        this.backColor = "rgba(0, 0, 0, 0.5)";
        this.mouseOverColor = "rgba(128, 143, 255, 0.44)";
        this.width = this.canvas.width;
        this.height = this.canvas.height;
        this.cX = this.width / 2;
        this.cY = this.height / 2;
        this.ctxFactor = (((this.startAngle - this.endAngle) / this.segments) * 11.5) / 100; //roundness
        this.gradient = null;

        this.target.onmouseover = function () {
            that.isbeingClosed = false;
            SetPosition(that);
            that.refresh();
        };

        document.addEventListener("click", function (evt) {
            if (that.alwaysVisible) return;
            that.isbeingClosed = true;
            fadeOut(that);
        }, false);

        window.addEventListener("resize", function (evt) {
            that.refresh();
        }, false);

        if (window.addEventListener) {
            window.addEventListener('load', function (evt) {
                that.refresh();
            }, false);
        } else if (window.attachEvent) {
            window.attachEvent('onload', function (evt) {
                that.refresh();
            }, false);
        }
        this.setStage(function () {
            if ((this.isbeingClosed == false && this.canvas.style.display == 'block') || this.alwaysVisible) {
                SetPosition(this);
                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();
                this.target.style.zIndex = (this.canvas.style.zIndex) + 1;
            }
        });
    };
CurvyTip.prototype.reset = function (evt) {
    if (!evt) {
        evt = window.event;
    }
    if (evt != null) {
        this.setMousePosition(evt);
    }
    this.regionIndex = 0;
    if (this.stage !== undefined) {
        this.stage();
    }
    // mouse flags
    this.mouseClick = false;
    this.mouseOver = false;
    this.mouseMove = false;
};
CurvyTip.prototype.listen = function () {
    var that = this;
    if (this.stage !== undefined) {
        this.stage();
    }
    this.canvas.addEventListener("click", function (evt) {
        that.mouseClick = true;
        that.reset(evt);
    }, false);
    this.canvas.addEventListener("mousemove", function (evt) {
        that.reset(evt);
    }, false);
    this.canvas.addEventListener("mouseover", function (evt) {
        that.mousePos = null;
    }, false);
    this.canvas.addEventListener("mouseout", function (evt) {
        that.mousePos = null;
    }, false);
};
CurvyTip.prototype.getMousePos = function (evt) {
    return this.mousePos;
};
CurvyTip.prototype.setMousePosition = function (evt) {
    var mouseX = evt.clientX - this.getCanvasPos().left + window.pageXOffset;
    var mouseY = evt.clientY - this.getCanvasPos().top + window.pageYOffset;
    this.mousePos = {
        x: mouseX,
        y: mouseY
    };
};
// ======================================= REGION EVENTS =======================================
CurvyTip.prototype.beginRegion = function () {
    this.currentRegion = {};
    this.regionIndex++;
};
CurvyTip.prototype.addRegionEventListener = function (type, func) {
    var event = 'on' + type;
    this.currentRegion[event] = func;
};
CurvyTip.prototype.closeRegion = function () {
    var pos = this.mousePos;
    if (pos != null && this.context.isPointInPath(pos.x, pos.y)) {
        if (this.lastRegionIndex != this.regionIndex && this.mouseOverRegionIndex != this.lastRegionIndex) {
            this.lastRegionIndex = this.regionIndex;
        }
        // handle onclick
        if (this.mouseClick && this.currentRegion.onclick !== undefined) {
            this.currentRegion.onclick();
            this.mouseClick = false;
        }
        // handle onmouseover 
		else if (!this.mouseOver && this.lastRegionIndex == -1 && this.regionIndex != this.mouseOverRegionIndex && this.currentRegion.onmouseover !== undefined) {
            this.currentRegion.onmouseover();
            this.mouseOver = true;
            this.mouseOverRegionIndex = this.regionIndex;
        }
    } else if (this.regionIndex == this.lastRegionIndex) {
        this.mouseOverRegionIndex = -1;
        this.lastRegionIndex = -1;
        if (this.currentRegion.onmouseout !== undefined) {
            this.currentRegion.onmouseout();
        }
    }
};
CurvyTip.prototype.getCanvasPos = function () {
    var obj = this.canvas;
    var top = 0;
    var left = 0;
    while (obj != null && obj.tagName != "BODY") {
        top += obj.offsetTop;
        left += obj.offsetLeft;
        obj = obj.offsetParent;
    }
    return {
        top: top,
        left: left
    };
};
CurvyTip.prototype.setStage = function (func) {
    this.stage = func;
    this.listen();
};
CurvyTip.prototype.clear = function () {
    this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
};
CurvyTip.prototype.refresh = function () {
    if (this.isbeingClosed) {
        return;
    }
    this.reset();
};
CurvyTip.prototype.setOptions = function (options) {
    var property;
    for (property in options) {
        if (!this.hasOwnProperty(property)) continue;
        this[property] = options[property];
        if (property == 'images') {
            this.setupImages();
        }
    }
    return this;
};
CurvyTip.prototype.setupImages = function () {
    var that = this;
    this.tiles = [];
    for (x = 0; x <= this.segments; x++) {
        var imageObj = new Image(); // new instance for each image
        imageObj.src = this.images[x];
        this.tiles.push(imageObj);
        imageObj.onload = function () {
            setTimeout(function () {
                that.refresh();
            }, 0);
        }
    }
    return this;
};
CurvyTip.prototype.addListener = function (type, func) {
    var event = 'on' + type;
    this.currentRegionEvent[event] = func;
    return false;
};
CurvyTip.prototype.removeListener = function (type, func) {
    var event = 'on' + type;
    this.currentRegionEvent[event] = undefined;
    return false;
};
CurvyTip.prototype.clearCvs = function () {
    if (this.isbeingClosed)
    return;
    this.context.clearRect(0, 0, this.width, this.height);
};
function GetAngle(instance, segment, pos) {
    var segangle = (100 / instance.segments) * (segment + pos);
    return ((instance.endAngle - instance.startAngle) * segangle) / 100 + instance.startAngle;
}
function getX(angle, r, cx) {
    return cx + (Math.cos(angle * TO_RADIANS) * r);
};
function getY(angle, r, cy) {
    return cy - (Math.sin(angle * TO_RADIANS) * r);
};
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);
    if (fol) {
        context.quadraticCurveTo(innerSX, innerSY, innerMPX, innerMPY);
        context.lineTo(innerMCX, innerMCY);
        context.lineTo(innerMBX, innerMBY);
        context.quadraticCurveTo(innerEX, innerEY, innerEX, innerEY);
    } else
    context.quadraticCurveTo(innerMX, innerMY, innerEX, innerEY);  
    context.quadraticCurveTo(innerEX, innerEY, outterEX, outterEY);
    context.quadraticCurveTo(outterMX, outterMY, outterSX, outterSY);
};
CurvyTip.prototype.drawCanvas = function () {
    if (this.gradient == null) {
        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.fillStyle = gradient;
    } else {
        this.context.fillStyle = this.gradient;
    }
    this.context.lineWidth = .5;
    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();
}
for (var i = 0; i < this.segments; i++) {
this.beginRegion();
this.context.beginPath();
drawSegment(this, i);
this.context.fill();
this.context.stroke();
this.context.closePath();
var that = this;
this.addRegionEventListener("mouseover", function () {
    that.hitSegment = i;
    if (that.currentRegionEvent.onmouseover !== undefined) {
        that.currentRegionEvent.onmouseover(i);
    }
});
this.addRegionEventListener("click", function () {
    if (that.currentRegionEvent.onclick !== undefined) {
        that.currentRegionEvent.onclick(i);
    }
});
this.addRegionEventListener("mouseout", function () {
    that.hitSegment = -1;
    if (that.currentRegionEvent.onmouseout !== undefined) {
        that.currentRegionEvent.onmouseout(i);
    }
});
this.closeRegion();
}
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 (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();
}
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();
}
}
};
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;
}
setOpacity(instance.canvas, 1);
instance.canvas.setAttribute("style", "display:block; left:" + left + "px; top:" + top + "px;z-index:9999");
}
function getOffset(obj) {
var output = new Object();
var mytop = 0,
myleft = 0;
while (obj) {
mytop += obj.offsetTop;
myleft += obj.offsetLeft;
obj = obj.offsetParent;
}
output.left = myleft;
output.top = mytop;
return output;
}
function setOpacity(element, level) {
element.style.opacity = level;
element.style.MozOpacity = level;
element.style.KhtmlOpacity = level;
element.style.filter = "alpha(opacity=" + (level * 100) + ")";
}
function fadeOut(element) {
var op = 1; // initial opacity
var timer = setInterval(function () {
if (op <= 0.1) {
    clearInterval(timer);
    element.canvas.style.display = 'none';
    element.isbeingClosed = false;
}
setOpacity(element.canvas, op);
op -= op * 0.1;
}, 15);
}

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

This article, along with any associated source code and files, is licensed under The MIT License

Share

About the Author

sistec

United States United States
No Biography provided

| Advertise | Privacy | Terms of Use | Mobile
Web04 | 2.8.150301.1 | Last Updated 21 Mar 2013
Article Copyright 2013 by sistec
Everything else Copyright © CodeProject, 1999-2015
Layout: fixed | fluid