0

I have 2 canvases. One, the main canvas. Upon which all is drawn, Second, the speech bubble canvas (balloon). Which displays information about specific regions on my main canvas upon client clicks.

I was playing around with my canvas after introducing the speech bubble and came across an issue.

This is a simple code that shows how the speech bubble is introduced:-

http://jsfiddle.net/m1erickson/AJvkN/

My canvas is a timeline, and is scrollable; has historical events plotted on it. Once a user clicks on an event a speech bubble appears.

Now what I don't want to happen is, when a client clicks on the canvas, a speech bubble appears and then scrolls, the speech bubble moves to a new position on the scrolled image, however still showing information about the previous location.

For this we have the hideballoon () which assigns css property left : -200. However this still causes inconsistencies. For example if I drag the canvas from left to right, the balloon doesn't disappear with the scroll, but reappears in a new position.

there is a .remove() function $("#balloon").remove()

http://api.jquery.com/remove/

This successfully removes the balloon however, the issue with this is:- it removes the balloon completely, and no future clicks will pop up any more speech bubbles. This is not what I want. I want something dynamic.

Click on event >> speech bubble appears >> scroll canvas >> speech bubble disappears >> click on canvas >> speech bubble pertaining to new click appears back >> and so on and so forth.

Philo
  • 1,931
  • 12
  • 39
  • 77

1 Answers1

1

[Edited]

Use .show() and .hide() to keep the balloon out of your way when its not needed

When the user scrolls the window then just hide the balloon.

I assume you're scrolling the window instead of the canvas. If you're scrolling the canvas, just use $("#canvas").scroll( ... ) instead.

So when you need the balloon:

        // move the balloon canvas to the target
        $("#balloon").css({left:offsetX+X, top:offsetY+Y});

        // and show it
        $("#balloon").show();

And hide the balloon when the user clicks on it or when the window scrolls:

    // listen for clicks on the balloon and then hide the balloon
    $("#balloon").click(function(e){ $("#balloon").hide(); });

    // listen for scrolls and then hide the balloon
    $(window).scroll(function(e){
        $("#balloon").hide(); 
    });

Here’s working sample code and a Fiddle: http://jsfiddle.net/m1erickson/uWHkv/

<!doctype html>
<html>
<head>
<link rel="stylesheet" type="text/css" media="all" href="css/reset.css" /> <!-- reset css -->
<script type="text/javascript" src="http://code.jquery.com/jquery.min.js"></script>

<style>
    body{ width:2000px; background-color: ivory; padding:10px;padding-top:100px; }
    #canvas{border:1px solid red;}
    #balloon{ position:absolute; left:-200px; }
</style>

<script>
$(function(){

    // get reference to our drawing canvas
    var canvas=document.getElementById("canvas");
    var ctx=canvas.getContext("2d");
    // get reference to our balloon canvas
    var balloon=document.getElementById("balloon");
    var popCtx=balloon.getContext("2d");

    // get the position of canvas relative to window
    var canvasOffset=$("#canvas").offset();
    var offsetX=canvasOffset.left;
    var offsetY=canvasOffset.top;

    // define some targets and their basic info
    var count=1;
    var circles=[];
    for(var x=50;x<1900;x+=50){
        circles.push({
            x:x,  y:120, radius:20,
            color:"blue",  
            info:"I'm #"+(count++)});
    }

    // draw the target circles on the canvas
    ctx.fillStyle="yellow";
    ctx.font="16px verdana";
    for(var i=0;i<circles.length;i++){
        drawCircle(circles[i]);
        ctx.beginPath();
        ctx.fillText(i+1,circles[i].x-8,circles[i].y+5);
    }

    // listen for clicks on the canvas and show the balloon
    $("#canvas").click(function(e){ 

        // get the mouseclick position
        mouseX=parseInt(e.clientX-offsetX);
        mouseY=parseInt(e.clientY-offsetY);

        // account for the window scrolling
        var scrollX=$(window).scrollLeft();
        var scrollY=$(window).scrollTop();

        // see if we clicked on any targets
        for(var i=0;i<circles.length;i++){
            var circle=circles[i];
            var dx=(circle.x-scrollX)-mouseX;
            var dy=(circle.y-scrollY)-mouseY;
            var radius=circle.radius;
            // true if we clicked in the target circle
            if(dx*dx+dy*dy<=radius*radius){
                drawBalloon(circles[i].x+radius,circles[i].y-100,circles[i].info);
            }
        }
    });


    // listen for clicks on the balloon and then hide the balloon
    $("#balloon").click(function(e){ $("#balloon").hide(); });

    // listen for scrolls and then hide the balloon
    $(window).scroll(function(e){
        $("#balloon").hide(); 
    });


    function drawCircle(circle){
        ctx.save();
        ctx.beginPath();
        ctx.fillStyle=circle.color;
        ctx.strokeStyle="black";
        ctx.lineWidth=3;
        ctx.arc(circle.x,circle.y,circle.radius,0,Math.PI*2,false);
        ctx.closePath();
        ctx.fill();
        ctx.stroke();
        ctx.restore();
    }


    function drawBalloon(X,Y,theInfo){
        popCtx.save();
        popCtx.fillStyle="#FD0";
        popCtx.strokeStyle="#000";
        // draw the balloon
        popCtx.beginPath();
        popCtx.moveTo(52,02);
        popCtx.quadraticCurveTo(02,02,02,42);
        popCtx.quadraticCurveTo(02,77,27,77);
        popCtx.quadraticCurveTo(27,102,07,102);
        popCtx.quadraticCurveTo(37,102,42,77);
        popCtx.quadraticCurveTo(102,77,102,42);
        popCtx.quadraticCurveTo(102,02,52,02);
        popCtx.lineWidth=3;
        popCtx.stroke();
        popCtx.fill();
        // draw theInfo
        popCtx.font="10pt arial";
        popCtx.fillStyle="black";
        popCtx.fillText(theInfo,10,50);
        popCtx.restore();
        // move the balloon canvas to the target
        $("#balloon").css({left:offsetX+X, top:offsetY+Y});
        $("#balloon").show();
    }

}); // end $(function(){});
</script>

</head>

<body>
    <canvas id="canvas" width=1950 height=300></canvas>
    <canvas id="balloon" width=105 height=105></canvas>
</body>
</html>
markE
  • 102,905
  • 11
  • 164
  • 176
  • marke, if you read my above description that is exactly what I am doing, and having problems with!!! what happens if I scroll, my canvas and then find the balloon again?? It needs to go away. – Philo May 08 '13 at 17:18
  • See my edited answer. PS--a downvote...I'm just trying to help :( – markE May 08 '13 at 17:36
  • i have tried all, properties: top, left, right and bottom, tried visibility: hidden. .remove(). None of them work as I want. by moving the bubble -200 left it is still there, and when I scroll my original canvas... the balloon re-appears at somepoint. this happens constantly, whether i scroll from left to right or right to left. Top attribute sometime delete the balloon and sometimes doesn.t very frustrating. – Philo May 08 '13 at 17:45
  • OK, if moving offstage is giving you an itch, how about just using $("#balloon").hide() and $("#balloon").show() instead of moving it offstage. This works in my demo code. If you're having problems, I might need to see more of your code...but I'm glad to help! – markE May 08 '13 at 18:12
  • .scroll doesn't work.. .click works. lol. this is so weird. thanks for the input tho. i will make something out of it. showing my code at the moment is not possible. about 3000 lines of canvas elements. lol. then sql. then C#. – Philo May 08 '13 at 18:48
  • Hate to leave you stranded, so I did a working example showing how to show/hide the tooltip even when the user scrolls the window: http://jsfiddle.net/m1erickson/uWHkv/ – markE May 08 '13 at 19:51
  • i am not using a window scroll, I am using something like this:- apparently jfiddle wouldn't run this correctly. but it works for me. http://jsfiddle.net/WNpKE/12/ – Philo May 08 '13 at 21:08
  • Interesting project...thanks for sharing a bit! It looks like you're using the **translated** accumulator to normalize your viewport. You could similarly normalize your tooltips. So in your hittest loop, you could normalize circle's X position like this: var dx=(circle.x-scrollX)-mouseX; – markE May 08 '13 at 21:29
  • what do you mean by hittest loop ? – Philo May 08 '13 at 21:52
  • i used hide and show functions throughout my code and solved the anomaly. Thanks for the insight. Basically I used hide() everywhere except for when there is a match between mouse click co-ordinates and balloon co-ordinates. If you think of a possible error with this approach do let me know. and Thank you for the insight on "show()" method. I didn't know it existed. – Philo May 08 '13 at 22:07
  • wow, we just arrived at the same conclusion simultaneously. !! – Philo May 08 '13 at 22:08
  • I do see 1 small glitch that needs cleaning. A balloon can extend above the canvas. If the user dismisses the balloon by clicking in the balloon above the canvas, the canvas loses "focus" and you might have to click twice to get the next balloon (1 click to "focus" the canvas and next click to request a balloon). Since your chart takes up much of the screen, this may not be an issue for you. – markE May 08 '13 at 22:10
  • one thing that I have noticed, before I didn't have all these balloon events per say. Now I have about 150 events (or balloons). This causes lag in my scrolling functionality. I was real happy with my scrolling earlier. now its bearable. – Philo May 08 '13 at 22:23
  • 1
    "Divide and conquer". Divide the circles of your timeline into sections and only hittest the likely visible circles. For example, if the user is viewing May 8th, you might only need test the circles from May 1st to may15th. BTW, you can cache the balloon outline as an image to speed up displaying a balloon. This speeds up drawing *dramatically* if you're drawing many balloons. – markE May 08 '13 at 22:40
  • The user is viewing a timeline say from May 1991 to current date. Frame by frame. All events are plotted before the timeline is displayed. any examples of cache-ing? – Philo May 08 '13 at 22:44
  • 1
    Caching an empty balloon as an image instead of redrawing with curves each time: http://jsfiddle.net/m1erickson/TA79F/ . In your "Frame by Frame", just hittest the circles that are inside the current frame. You can build the circle objects ahead of time, but just do the hittesting on the current frame's circles. Auto-moderator is getting annoyed with our extended discussion...bye for now! – markE May 08 '13 at 23:10
  • Thanks! this has been a great learning experience. All of this is really cool. Maybe One last thing:- Before this question/answer session with you, I was simply drawing my events on the canvas as circles. Now my circles are more like objects, with related information stored in its properties. Now my canvas may have 258 circles on it, which makes scrolling considerably choppy.. before introducing the events as objects, my scrolling was very smooth. Any way to implement a similar analogy to cached balloon image, so that I can make scrolling faster and smoother? – Philo May 09 '13 at 17:49
  • Yes, here’s the secret to speed! **The hittesting of circles is what’s taking time—remember, you’re doing 258 hittests**, so (1) Only hittest circles if your *not dragging*. (2) Create your 258 circles in advance (that’s not what’s taking time), but *hittest only those circles currently visible on the screen*. You already know the “X” position of each circle along the timeline. Compare that X to Translated to determine whether to hittest any particular circle. – markE May 09 '13 at 22:34
  • Another secret to speed! The act of dragging causes a mousedown event which is interpreted as a “click”. No hittesting is necessary during dragging. So move your balloon trigger from “click” to “mouseup” and then don’t do the hittesting if your dragging=true flag is set. In other words, trigger your balloons in mouseup and only hittest if you’re not dragging. – markE May 09 '13 at 22:34
  • this is my current balloon trigger:_ jsfiddle.net/TA79F/3/ looks good? what did u mean by hittest only the circles visible on the screen? aka- check what position of X is visible on timeline and then only look at circles in that region? – Philo May 10 '13 at 22:47
  • how do I make the quadratic curve speech balloon bigger to fit more information in it? I don't quite understand the math that goes behind it. – Philo May 13 '13 at 18:48
  • Hi Philo, we have to stop meeting like this...the guards are getting suspicious that we're planning a prison break!! You can add some horizontal space to your speech bubbles by adding horizontal lines between the quadratic curves. Here's an example: http://www.scriptol.com/html5/canvas/speech-bubble.php – markE May 13 '13 at 19:37
  • Is there a better way to reach you? – Philo May 13 '13 at 19:41
  • hey MarkE, need a bit more help with this question:- http://stackoverflow.com/questions/16658416/syncrhonous-scrolling-of-2-separate-canvases-on-a-single-page can u take a look? – Philo May 20 '13 at 23:46
  • Hey MarkE, sent a gmail to your addy 'marksStackoverflowAddress@GmailDotCom'. 1:- when scrolling both canvases together, objects on canvas2 disappear until scrolling is complete. and then appear back again. 2:- when not scrolling, but just mousedown on canvas1, redundant objects start appearing on canvas2. 3:- Canvas2 y-axis labels need to be dynamic. eg:- for a particular period on the timeline, there could be 5 items on the y axis label, for another period there could be 50. This seems pretty ridiculous for a web app. can we discuss these? – Philo Jun 04 '13 at 17:48