3
\$\begingroup\$

I've not done JavaScript in quite a while, but I wanted to make it so that I can circle (in many different ways) around the bounding box of a tagged item.

Is this a good way of doing what I'm wanting?

Is there a better way? E.g. can this be done with only CSS?

Or better style? E.g. can I use better, more understandable idioms?

Did I forget anything? E.g. Am I missing a case where it won't resize the circle correctly?

Please remember that e.g. means 'for the sake of example', which means that I'm only giving an example, and that there might be something else that falls into the category specified that you might think of.

http://jsfiddle.net/adrianh/4SVHH/27/

// Creating a tag with the class 'circled' will have that element circled
// using a number of different ways by setting data-style attribute to:
//   'default'  - an open ellipse (default)
//   'ellipse'  - a closed ellipse
//   'circle'   - a closed circle circumscribed around the bounding box.
//   'rect'     - a closed rectangle
//   'vbracket' - a square bracket like [ ]
//   'hbracket' - a square bracket that is rotated 90 degrees
//
// Other attributes are:
//   data-width - width of lines used to circle element. (default='2')
//   data-style - can be: 'solid', 'dotted' or 'dashed'. (default='solid')
//   data-color - any html color. (default='red')
//
// Note: The circle is around the bounding box of the tagged object,
//       so if the object wraps, it will span the paragraph width.
//
// Note: Circle will be redrawn if window is resized.

(function()
{
    // draws an ellipse
    function drawEllipse(context, centerX, centerY, width, height)
    {
        var ratio = height/width;
        context.save();
        context.scale(1,  ratio);
        context.beginPath();
        context.arc(centerX, centerY/ratio, width/2, 0, 2 * Math.PI);
        context.restore();
    };

    // returns the appropriate array for the style requested
    function getLineDash(style)
    {
        if (style == "solid")
            return [];
        if (style == 'dotted')
            return [3];
        if (style == 'dashed')
            return [6, 3];
        return [];
    };

    // points at object that contains all the circle canvases
    var circles;

    // Return a canvas given an indexed number.  If the same
    // number is used multiple times, then that canvas is reused.
    // Canvas is positioned in the (left, top) coordinate in the
    // document with the specified width and height.
    function getCanvas(i, left, top, width, height)
    {
        var canvas;
        if (typeof circles.childNodes[i] != 'undefined')
        {
            canvas = circles.childNodes[i];
            canvas.width=width;
            canvas.height=height;
            canvas.style.left=left+"px";
            canvas.style.top=top+"px";
        }
        else
        {
            canvas = "<canvas "+
            "width='"+width+"' "+
            "height='"+height+"' "+
            "style='"+
                "position:absolute;"+
                "z-index:0;"+
                "left:"+left+"px;"+
                "top:"+top+"px;"+
                "pointer-events:none;"+
                //"border:1px solid;"+
            "' />";
            circles.insertAdjacentHTML('beforeend', canvas);
        }
        return circles.childNodes[i];
    };

    // draw a circle circumscribed around the rect
    function circleRect(i, rect, lineWidth, lineColor, lineStyle)
    {
        var diameter = Math.sqrt(rect.width*rect.width+rect.height*rect.height);
        var cx = (rect.right + rect.left)/2;
        var cy = (rect.top + rect.bottom)/2;
        var left = Math.floor(cx - diameter/2);
        var top  = Math.floor(cy - diameter/2);
        diameter = Math.floor(diameter);
        var canvas = getCanvas(i, left-1, top-1, diameter+2, diameter+2);
        var ctx=canvas.getContext("2d");
        ctx.beginPath();
        ctx.arc(diameter/2+1,diameter/2+1,diameter/2,0,2*Math.PI);
        ctx.lineWidth   = lineWidth;
        ctx.strokeStyle = lineColor;
        ctx.setLineDash = getLineDash(lineStyle);
        ctx.stroke();
    };

    // draw an open ellipse around the rect.
    function defaultRect(i, rect, lineWidth, lineColor, lineStyle)
    {
        var quarterWidth = rect.width/4;
        var quarterHeight = rect.height/4;
        var eighthWidth = quarterWidth/2;
        var eighthHeight = quarterHeight/2;
        var padx = quarterWidth;
        var pady = quarterHeight*1.5;
        var width = rect.width;
        var height = rect.height;
        var canvas = getCanvas(i, rect.left-padx, rect.top-pady, width+padx*2, height+pady*2);
        //canvas.style.border="1px solid yellow";
        var ctx=canvas.getContext("2d");
        ctx.beginPath();
        ctx.moveTo(       padx+width/2,             pady+eighthHeight);
        ctx.bezierCurveTo(padx-quarterWidth,        pady+quarterHeight/3,
                          padx,                     pady+height+eighthHeight,
                          padx+width/2-eighthWidth, pady+height+eighthHeight);
        ctx.bezierCurveTo(padx+width+quarterWidth,  pady+height+quarterHeight,
                          padx+width+eighthWidth,   pady-eighthHeight*3,
                          padx+width/2-eighthWidth, pady-quarterHeight);
        ctx.lineWidth   = lineWidth;
        ctx.strokeStyle = lineColor;
        ctx.setLineDash(getLineDash(lineStyle));
        ctx.stroke();
    //    ctx.strokeStyle = "blue";
    //    ctx.lineWidth = 1;
    //    ctx.strokeRect(padx, pady, width, height);
    };

    // draw an ellipse around the rect
    function ellipseRect(i, rect, lineWidth, lineColor, lineStyle)
    {
        var padx = 200;
        var pady = 200;
        var canvas = getCanvas(i, rect.left-padx, rect.top-pady, rect.width+padx*2, rect.height+pady*2);
        var ctx=canvas.getContext("2d");
        //canvas.style.border = "1px blue solid"
        drawEllipse(ctx, rect.width/2+padx, rect.height/2+pady, rect.width, rect.height);
        ctx.lineWidth   = lineWidth;
        ctx.strokeStyle = lineColor;
        ctx.setLineDash(getLineDash(lineStyle));
        ctx.stroke();
    };

    // Draw a rectangle around the rect.
    function rectRect(i, rect, lineWidth, lineColor, lineStyle)
    {
        var half = Math.ceil(lineWidth/2);
        var padx = lineWidth;
        var pady = lineWidth;
        var canvas = getCanvas(i, rect.left-padx, rect.top-pady, rect.width+padx*2, rect.height+pady*2);
        //canvas.style.border = "1px blue solid";
        var ctx=canvas.getContext("2d");
        ctx.lineWidth   = lineWidth;
        ctx.strokeStyle = lineColor;
        ctx.setLineDash(getLineDash(lineStyle));
        ctx.strokeRect(padx-half, pady-half, rect.width+lineWidth/2, rect.height+lineWidth/2);
    };

    // Draw a rectangle around the rect.
    function vbracketRect(i, rect, lineWidth, lineColor, lineStyle)
    {
        var half = Math.ceil(lineWidth/2);
        var padx = lineWidth*2;
        var pady = lineWidth*2;
        var canvas = getCanvas(i, rect.left-padx, rect.top-pady, rect.width+padx*2, rect.height+pady*2);
        //canvas.style.border = "1px blue solid";
        var ctx=canvas.getContext("2d");
        ctx.lineWidth   = lineWidth;
        ctx.strokeStyle = lineColor;
        ctx.setLineDash(getLineDash(lineStyle));
        ctx.beginPath();
        ctx.moveTo(padx-half+half*3            , pady-half);
        ctx.lineTo(padx-half                   , pady-half);
        ctx.lineTo(padx-half                   , pady+half+rect.height);
        ctx.lineTo(padx-half+half*3            , pady+half+rect.height);
        ctx.moveTo(padx+half+rect.width-half*3 , pady-half);
        ctx.lineTo(padx+half+rect.width        , pady-half);
        ctx.lineTo(padx+half+rect.width        , pady+half+rect.height);
        ctx.lineTo(padx+half+rect.width-half*3 , pady+half+rect.height);
        ctx.stroke();
    };

    function hbracketRect(i, rect, lineWidth, lineColor, lineStyle)
    {
        var half = Math.ceil(lineWidth/2);
        var padx = lineWidth*2;
        var pady = lineWidth*2;
        var canvas = getCanvas(i, rect.left-padx, rect.top-pady, rect.width+padx*2, rect.height+pady*2);
        //canvas.style.border = "1px blue solid";
        var ctx=canvas.getContext("2d");
        ctx.lineWidth   = lineWidth;
        ctx.strokeStyle = lineColor;
        ctx.setLineDash(getLineDash(lineStyle));
        ctx.beginPath();
        ctx.moveTo(padx-half                   , pady-half+half*3);
        ctx.lineTo(padx-half                   , pady-half);
        ctx.lineTo(padx+half+rect.width        , pady-half);
        ctx.lineTo(padx+half+rect.width        , pady-half+half*3);
        ctx.moveTo(padx-half                   , pady+half+rect.height-half*3);
        ctx.lineTo(padx-half                   , pady+half+rect.height);
        ctx.lineTo(padx+half+rect.width        , pady+half+rect.height);
        ctx.lineTo(padx+half+rect.width        , pady+half+rect.height-half*3);
        ctx.stroke();
    };

    // if x is null return _default, otherwise return x.
    function val(x, _default)
    {
        return (x == null) ? _default : x;
    };

    // This draws the circles around all 'circled' class objects.
    function drawCircles()
    {
        $(".circled").each(function(i, obj) {
            var rect  = obj.getBoundingClientRect();
            var type  = val(obj.getAttribute("data-type"), "default");
            var width = val(obj.getAttribute("data-width"), 2);
            var color = val(obj.getAttribute("data-color"), "red");
            var style = val(obj.getAttribute("data-style"), "solid");
            if (type == 'default')
            {
                defaultRect(i, rect, width, color, style);
            }
            else if (type == 'rect')
            {
                rectRect(i, rect, width, color, style);
            }
            else if (type == 'ellipse')
            {
                ellipseRect(i, rect, width, color, style);
            }
            else if (type == 'vbracket')
            {
                vbracketRect(i, rect, width, color, style);
            }
            else if (type == 'hbracket')
            {
                hbracketRect(i, rect, width, color, style);
            }
            else if (type == 'circle')
            {
                circleRect(i, rect, width, color, style);
            }
        });
    };

    // Place a div container to place all of the canvases in.
    document.body.insertAdjacentHTML('afterbegin', '<div id="circles"></div>');
    circles = document.getElementById("circles");

    // Draw the circles upon loading the document and 
    // if the window extents have changed.
    window.onload=drawCircles;
    window.onresize=drawCircles;
})();
\$\endgroup\$
3
\$\begingroup\$

The data-* attributes are saved in the Element.prototype.dataList property, you can retrieve them like element.dataList['width']. Furthermore, since you use jQuery you can simply use the .data() method of jQuery:

var rect  = obj.getBoundingClientRect();
var type  = val(obj.data("type"), "default");
var width = val(obj.data("width"), 2);
var color = val(obj.data("color"), "red");
var style = val(obj.data("style"), "solid");

And since all functions have the same arguments, I would rather do something like this:

var cycleFunc;

switch (type) {
    case 'default':  cycleFunc = defaultRect;  break;
    case 'rect':     cycleFunc = rectRect;     break;
    case 'ellipse':  cycleFunc = ellipseRect;  break;
    case 'vbracket': cycleFunc = vbracketRect; break;
    case 'hbracket': cycleFunc = hbracketRect; break;
    case 'circle':   cycleFunc = circleRect;   break;
}
cycleFunc(i, rect, width, color, style);
\$\endgroup\$
  • \$\begingroup\$ I was actually considering removing jQuery references. I'm also on the fence if an if statement is better or worse than a switch/case statement. But this is good stuff to know. Thanks. \$\endgroup\$ – Adrian May 22 '14 at 19:04
  • 1
    \$\begingroup\$ The obj variable in this case is a reference to a DOM node, not a jQuery object. Plus, for something as simple as calling getAttribute and returning a string value, jQuery becomes overkill. \$\endgroup\$ – Greg Burghardt May 23 '14 at 17:27
2
\$\begingroup\$

Since all the *Rect() functions take the same arguments, create an object that references each function, then construct the method name based on the data-type attribute:

JsFiddle: http://jsfiddle.net/4SVHH/28/

Basically, after all the *Rect functions:

var decorations = {
    circleRect: circleRect,
    defaultRect: defaultRect,
    ellipseRect: ellipseRect,
    rectRect: rectRect,
    vbracketRect: vbracketRect,
    hbracketRect: hbracketRect
};

Then drawCircles becomes much shorter:

function drawCircles() {
    $(".circled").each(function(i, obj) {
        var rect  = obj.getBoundingClientRect();
        var type  = val(obj.getAttribute("data-type"), "default");
        var width = val(obj.getAttribute("data-width"), 2);
        var color = val(obj.getAttribute("data-color"), "red");
        var style = val(obj.getAttribute("data-style"), "solid");
        var method = type + "Rect";

        if (method in decorations) {
            decorations[method](i, rect, width, color, style);
        }
    });
}

As an added bonus, adding a new decoration/drawing style becomes trivial. Add a new value in a data-type attribute, and then create a new function using <data-type value>Rect as the name of the function. No need to update drawCircles().

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.