3

I am trying to code a builder for table tennis exercise graphics. I want it to show a table for each rally of the exercise, with arrows on it that show the movement path of the ball and rectangles of areas where to play.

Right now it looks like this: https://codepen.io/graNite/pen/grqXOo

You can add new tables, remove the last one, and draw two fixed arrows on the same canvas layer on the first table.

What I want to enable is to draw arrows* on each table via click-and-drag and delete them with right-click on them.

*(like they are drawn in the drawArrows function)

What is the best way to do this?

I already implemented a canvas layer for the arrows and could even do so for every single arrow on each table, but how can I access an arrow that is in a middle layer if I right click on it to delete it?

HTML

<button onclick="addTable()">Add table</button>
<button onclick="removeTable()">Remove table</button>
<button onclick="drawArrow(50, 50, 150, 250)">Draw arrow</button>
<button onclick="drawArrow(50, 300, 180, 20)">Draw arrow2</button>
</br>
<div id="tables">
</div>

CSS

body {
    background-color: #982439;
}

#table {
    padding: 10px;
}

canvas {
    position: absolute;
}

JS

function drawTable(table) {
    "use strict";
    var draw = table.getContext("2d");
    draw.shadowBlur = 20;
    draw.shadowColor = 'rgba(0,0,0,0.3)';            // shadow
    draw.fillStyle = "#2e3f73";                      // table
    draw.fillRect(35.25,    20,      152.5,  274);
    draw.fillStyle = "#ffffff";                      // lines
    draw.fillRect(111.35,   20,      0.3,    274);   // middle line
    draw.fillRect(35.25,    20,      2,      274);   // lift side
    draw.fillRect(185.75,   20,      2,      274);   // right side
    draw.fillRect(35.25,    20,      152.5,  2);     // top base line
    draw.fillRect(35.25,    292,     152.5,  2);     // bottom base line
    draw.fillRect(20,       156,     183,    2);     // net
}

function addTable() {
    "use strict";
    var container = document.createElement("div"),
        table = document.createElement("canvas"),
        arrowLayer = document.createElement("canvas"),
        width = 223,
        height = 314;


    container.appendChild(table);
    container.appendChild(arrowLayer);
    container.style.width = width + "px";
    container.style.height = height + "px";
    container.style.display = "inline-block";
    document.getElementById("tables").appendChild(container);

    table.width = width;
    table.height = height;
    table.className = "table";
    table.style.zIndex = "0";
    drawTable(table);

    arrowLayer.width = width;
    arrowLayer.height = height;
    arrowLayer.className = "arrow";
    arrowLayer.style.zIndex = "1";
    arrowLayer.id = "arrow1";
}

function removeTable() {
    "use strict";
    var child = document.getElementById("tables").lastChild;
    child.parentNode.removeChild(child);
}

function drawArrow(start_x, start_y, end_x, end_y) {
    "use strict";
    var draw = document.getElementById('arrow1').getContext('2d'),
        angle = Math.atan((end_y - start_y) / (end_x - start_x)),
        length = Math.sqrt(Math.pow((end_x - start_x), 2) + Math.pow((end_y - start_y), 2));

    // set colors and style
    draw.strokeStyle = "#ffb900";
    draw.fillStyle = "#ffb900";
    draw.lineWidth = 9;

    // draw arrow line
    draw.beginPath();
    draw.translate(start_x, start_y);
    draw.moveTo(0, 0);
    draw.rotate(angle);
    draw.lineTo(length - 23, 0); // note: arrowhead is 24px long and total arrow is line+head   
    draw.stroke();
    draw.moveTo(-start_x, -start_y);

    // draw arrow head
    draw.beginPath();
    draw.moveTo(length, 0);
    draw.lineTo(length - 24, -7.5); // ^ see note above
    draw.lineTo(length - 24, 7.5);
    draw.fill();

    //reset context 
    draw.rotate(-angle);
    draw.moveTo(-start_x, -start_y);
    draw.translate(-start_x, -start_y);
}
Tweakimp
  • 393
  • 5
  • 16
  • See this previous [Q&A](http://stackoverflow.com/questions/29692134/how-to-delete-only-a-line-from-the-canvas-not-all-the-drawings/29704300#29704300) to find which line is closest to your mouse. Then handle that line as your design requires. – markE Aug 14 '16 at 17:35

2 Answers2

2

Good interface design

A good quality interface should not have ugly buttons, it should be intuitive and easy to use. There should always be feed back (cursors, highlighting, roll over effects). The effects are not as much for show but to provide information users need to interact with the app. Thus the FX's do not have to be major, just enough for a user to see that everything is working and what is clickable and what is not.

I was going to add it without help as it should make sense but I have added help just incase it does not. We all have a different approch to interface usage.

UPDATE : I have added some more code. Help now only shown until the help feature is use. Once use the help is not display.

Added comments though code where I thought it needed it (basicly everywhere)

Code

Tables are in an array tableArray. Tables are added with addTable() which returns a table object. Tables default to inactive table.active=false. To activate click the mouse on the table or table.active = true; table.draw(); Arrows are in the array table.arrows as {x:?,y?,xx:?,yy:?,highlight:false} to remove use array splice or via the interface. To remove a table deactivate it table.active = false; (or click close) it will stay in the DOM untill updateTables() is called. (when closed by user mouse click update is called automatically)

There will always be one inactive table visible that can be used to add a active table.

Each table in the array has everything needed to do its thing. Mouseevent, render events, close, etc.

Each table is rendered at full frame rate (when it has internal mouse focus) so you can add nice animations if you want. When the mouse is not over Table it is not updated unless you call the table.draw() function.

The code is a bit messy, as it got a little out of hand. Consts define most things (at top of code). Table, close icon, and empty table are pre-rendered. Arrows and help are rendered as needed.

To use

Click an empty table to add (activate). Click on active tables close icon to close. Click drag to add arrow, Right click when near arrow to delete. Arrow near mouse will highlight.

Note The number of tables has no restriction. This is not a good idea and you should limit the number of tables. Also there is a bit of a layout issue when added tables cause page scroll bars to appear. Removing tables does not revert to what was. As I do not know what you want I left that for you to sort out.

Also added GLOBAL_SCALE const that is applied to all constants just for the fun of it so it is a little smaller that the original

// contains an array of tables.
var tableArray = [];

// App constants all up top
const GLOBAL_SCALE = 0.7;
const SHOW_HELP = true;  // set to false to have the help turned off
const SHADOW = 'rgba(0,0,0,0.8)';
const WHITE = "white";
const TABLE_REFRESH_DELAY = 50; // Time in millisecond befor updating DOM for table add and remove
const FONT = {
face : "px Arial",
size : Math.max(10,18 * GLOBAL_SCALE),
fill : WHITE,
};
const TABLE = {
width  : 223 * GLOBAL_SCALE,  // size of table
height : 314 * GLOBAL_SCALE,
tables : document.getElementById("tables"),
image : { // table image styles
    shadow : SHADOW,
    shadowBlur : 20 * GLOBAL_SCALE,
    fill :  "#2e3f73",
    lines : WHITE,
    font : FONT,
    cursor : "default",
},
empty : {  // empty table styles
    inset : 30 * GLOBAL_SCALE, // amount box is inset
    lines : 'rgba(255,255,255,0.5)',
    lineWidth : 8 * GLOBAL_SCALE,
    shadow : SHADOW,
    shadowBlur : 20 * GLOBAL_SCALE,
    font : FONT,   
    cursor : "pointer",
    highlightAmount : 0.3, // amount to highlight empty table when mouse over 0 none 1 full
},
arrow : {  // arrow styles
    width : 15 * GLOBAL_SCALE, // arrow width
    shadow : SHADOW,
    shadowBlur : 10 * GLOBAL_SCALE,
    // custom cursor
    cursor : "url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADcAAAAVCAYAAADiv3Z7AAACtUlEQVRYha2Ye1NTMRDFf/KqLY/SlvIolAJFqSKOoqI4o+P3/1LXP3oi23XvTUrpzJm52Zxs9mRzczeFl/u9KkDp2FV5xb/IURT42hIoGbcsp1hMGrxegw1hU9gCWsDrDFriWrTc2FYDL/K1qZiS2KywdRO0DcAiTdIBtoEdYFfYC7Arzo7426a96+B53n/idBTDlha7VmAStiGyD9wjiegCPWAAHABDg0NhqL6B0BcGZpyF5fQM98D52lcc7SaBXlhbArrGeRTQEDgCKoNzwdsq4Aw4BUZqn8qWeGOhMryRGWt9jrRwfSMwbdF/72F6SFuxLXLPORppwspMPgYq+0v93mb63rh2OFbPU9sO5hlrcXtKRstnz2atxXwb9oCqEhYmWrRdR4F6mxdQIzqyfWjwOQOutMgHzF+RlL0FcTZrXaV77MU4YZ+EXIC/InuTuIL+B809Ay6UvX3mZ4TdmgviOiIdARPgxooywr4BX4TGbRkFvoo40/4MvHPi/HtXK+5CKxOJ+y6BXwsC/ePav18gcz+AeyeuS2ZbenFN2zJlLxuUa4fbtIkTtFfOnBfzENjum8TZbWQ4j5nt9jPgPEY+KXzn1ng6UPaYnz7p+1JphW7R6WVsM6FS/x1Ph41d5dT+KB+3at/JVrn+9/Jf6fnWjEm4ofC0jD4Fhxo4kZOpwxVwaTBl/g17q4lnDjfqnxp/17IlXMt+qYxc6NnyLafoO1f3ER8Cxyx+xG3lcKL+E9N/pknPtTATPY/VNwr8nbFYvRw7nDj+qWzZCsVnr6T8snVfj//rv1SaRbVlPxhj0Wf+/lhE3MTL1paRwFzh7Kv2qMqPbgUR3/vsOET+l7oVWIElV56Su9ky9znvczPjf+n7nBUZ3YKjW/FzbuO5MXV/U6x8E68TmrP5vuf+h1IaT5b7F+ZaSjBzrT+rAAAAAElFTkSuQmCC') 10 11, pointer",
    fill : "#ffb900",
    highlight : "#ffdc44",
    lineWidth : 1,
    line : "#ffdc44",
    lineHigh : "#ffed55",
    head : 30 * GLOBAL_SCALE, // arrow head width
    minSize : 5, // min size arrow can be if smaller then arrow is not created
},
DOM : {  // variouse dom setting for table canvas and div tags
    display : "inline-block",
    canvasClass : "table",
    zIndex : 1,
},
closeIcon : { // styles for reandering and display close icon
    size : 32 * GLOBAL_SCALE,
    fill : "red",
    lines : WHITE,
    lineWidth : Math.max(1,2 * GLOBAL_SCALE),
    shadow : SHADOW,
    shadowBlur : 20 * GLOBAL_SCALE,  
    cursor : "pointer",
    pos : {
        x : 1, // as fractions
        y : 0, 
    }
},
help : {  // text help
    empty : "Click here to|add a new table".split("|"),
    active : "Click to drag arrows".split("|"),
    activeArrow : "Right click on arrow|to remove it".split("|"),
    closeTable : "To close table|move to top right|click Close Icon".split("|"),
}
}
const MOUSE = {  // event contains a list of mouse event to listen to 
buttonMasks : [1, 2, 4, 6, 5, 3],
events : "mousemove,mousedown,mouseup,mouseout,mouseover,contextmenu".split(","),
};  // contextmenu is included as that needs to be blocked for right button events

var helpItemsUsed = {
empty : false,
active : false,
activeArrow : false,
closeTable : false,
};
const turnOffHelp = function(){
helpItemsUsed.empty = true;
helpItemsUsed.active  = true;
helpItemsUsed.activeArrow  = true;
helpItemsUsed.closeTable  = true;
};
if(!SHOW_HELP){turnOffHelp();};
// returns distance of point p to line segment x, y,xx,yy
const distFromLine = function(px,py,x,y,xx,yy){
var vx,vy,pvx,pvy,lx,ly,u;
vx = xx - x;
vy = yy - y;
pvx = px - x;
pvy = py - y;
u = (pvx * vx + pvy * vy)/(vy * vy + vx * vx);
if(u >= 0 && u <= 1){
    lx = vx * u;
    ly = vy * u;
    return Math.sqrt(Math.pow(ly - pvy,2) + Math.pow(lx - pvx,2));
}
// closest point past ends of line so get dist to closest end
return Math.min(
    Math.sqrt(Math.pow(xx - px,2)+ Math.pow(yy - py,2)),
    Math.sqrt(Math.pow(x - px,2)+ Math.pow(y - py,2))
);
}
// set up functions create images and do other general setup
function setupContext(ctx,descript){ // sets common context settings
ctx.shadowBlur = descript.shadowBlur;
ctx.shadowColor = descript.shadow;   
ctx.strokeStyle = descript.lines;          
ctx.fillStyle = descript.fill;          
ctx.lineWidth = descript.lineWidth;    
ctx.lineCap = "round";    
if(descript.font){
    ctx.font = descript.font.size + descript.font.face;
}
}
function createTableImage() {  // create image of table but why write a comment when the function tells it all???
var table = document.createElement("canvas");
table.width = TABLE.width;
table.height= TABLE.height;
var ctx = table.getContext("2d");
setupContext(ctx,TABLE.image);
var scaleX = table.width / 223; /// get the scale compared to original layout
var scaleY = table.height / 314; /// get the scale compared to original layout
ctx.fillStyle = TABLE.image.fill; 
ctx.fillRect(35.25 * scaleX,    20 * scaleY,      152.5 * scaleX,  274 * scaleY);
ctx.fillStyle = TABLE.image.lines;            // lines
ctx.fillRect(111.35 * scaleX,   20 * scaleY,      0.3,    274 * scaleY);   // middle line
ctx.fillRect(35.25 * scaleX,    20 * scaleY,      2,      274 * scaleY);   // lift side
ctx.fillRect(185.75 * scaleX,   20 * scaleY,      2,      274 * scaleY);   // right side
ctx.fillRect(35.25 * scaleX,    20 * scaleY,      152.5 * scaleX,  2);     // top base line
ctx.fillRect(35.25 * scaleX,    292 * scaleY,     152.5 * scaleX,  2);     // bottom base line
ctx.fillRect(20 * scaleX,       156 * scaleY,     183 * scaleX,    2);     // net
return table
}

function createEmptyImage() { // empty table image
var i = TABLE.empty.inset;
var image = document.createElement("canvas");
var w = image.width = TABLE.width;
var h = image.height = TABLE.height;
var ctx = image.getContext("2d");
setupContext(ctx,TABLE.empty);
ctx.strokeRect(i,    i, w - i * 2, h - i * 2);
ctx.beginPath();
ctx.moveTo(i * 2, i * 2);
ctx.lineTo(w - i * 2, h - i * 2);
ctx.moveTo(i * 2, h - i * 2);
ctx.lineTo(w - i * 2, i * 2);
ctx.stroke();
return image
}
function createCloseImage() {  // create close icon
var S = TABLE.closeIcon.size;
var s = S * 0.5;
var c = s * 0.4; // cross dist from center
var sb = TABLE.closeIcon.shadowBlur;
var l = TABLE.closeIcon.lineWidth;
var image = document.createElement("canvas");
// Image must include shadowblur
image.width = S+sb; // add blur to size
image.height= S+sb;
var ctx = image.getContext("2d");
setupContext(ctx,TABLE.closeIcon);
ctx.beginPath();
var cx = s + sb / 2;  // add half blur to get center
var cy = s + sb / 2;
ctx.arc(cx, cy, s - l, 0, Math.PI * 2);
ctx.fill();
ctx.stroke();
ctx.beginPath();
ctx.moveTo(cx - c, cy - c)
ctx.lineTo(cx + c, cy + c)
ctx.moveTo(cx - c, cy + c)
ctx.lineTo(cx + c, cy - c)
ctx.stroke();
return image
}
// create the images
var tableImage = createTableImage();
var closeIcon = createCloseImage();
var emptyTableImage = createEmptyImage();


// draws a arrow a is the arrow object
function drawArrow(ctx,a){
var s = TABLE.arrow; // get arrow style
var vx,vy;
var x,y;
x = a.x;
y = a.y;
vx = a.xx-x;
vy = a.yy-y;
var dir = Math.atan2(vy,vx);
var len = Math.sqrt(vx * vx + vy * vy);
// ctx.save();
ctx.setTransform(1,0,0,1,x,y);
ctx.rotate(dir);
var w = s.width/2;
var h = Math.min(len,s.head); // ensure arrow head no bigger than arrow length
h /=2;
if(a.highlight){
    ctx.fillStyle = s.highlight;
    ctx.strokeStyle = s.lineHigh;
}else{
    ctx.fillStyle = s.fill;
    ctx.strokeStyle = s.line;
}
ctx.lineWidth = s.lineWidth;
ctx.save();
ctx.shadowBlur = s.shadowBlur;
ctx.shadowColor = s.shadow;       
ctx.beginPath();
ctx.moveTo(0,-w/2);
ctx.lineTo(len-h-h,-w);
ctx.lineTo(len-h-h,-h);
ctx.lineTo(len,0);
ctx.lineTo(len-h-h,h);
ctx.lineTo(len-h-h,w);
ctx.lineTo(0,w/2);
ctx.closePath();
ctx.fill();
ctx.stroke();
ctx.restore();
}

// display help text for table
function drawHelpText(ctx,text,style){
ctx.font = style.font.size + style.font.face;
ctx.textAlign = "center";
ctx.textBaseline = "middle";
var i,len;
len = text.length;
var y = ctx.canvas.height / 2 - len * style.font.size * 1.2;
var yy = y + 1;
ctx.strokeStyle = "#000";
ctx.lineWidth = 2;
for(i = 0; i < len; i++){
    ctx.strokeText(text[i], ctx.canvas.width / 2 + 1, yy);
    yy += TABLE.empty.font.size * 1.2;
}     
ctx.fillStyle = style.font.fill;
for(i = 0; i < len; i++){
    ctx.fillText(text[i], ctx.canvas.width / 2, y);
    y += TABLE.empty.font.size * 1.2;
} 

}
//------------------------------------------------------------
// functions for table
function drawClose(){ // draws close icon. Fades in the close mouse is
var ctx = this.ctx;
var w = closeIcon.width;
var grow = w * 0.1;
var x = (this.width - w) * TABLE.closeIcon.pos.x ;
var y = (this.height - w) * TABLE.closeIcon.pos.y ;

x += w/2; // get icon center 
y += w/2;
var dist = Math.sqrt(Math.pow(this.mouse.x - x, 2) + Math.pow(this.mouse.y - y, 2));
if(dist < TABLE.closeIcon.size / 2){
    this.mouseOverClose = true;
}else{
    this.mouseOverClose = false;
}
x -= w/2; // back to icon top left
y -= w/2;
ctx.globalAlpha = 1-(Math.min(100,(dist - w * 2)) / 100);
if(this.mouseOverClose){
    ctx.drawImage(closeIcon,x-grow,y-grow,w + grow * 2,w + grow * 2);
}else{
    ctx.drawImage(closeIcon,x,y);
}
ctx.globalAlpha = 1;
}
function drawEmpty(){ // draw empty table and handle click on empty table
var ctx = this.ctx;
ctx.drawImage(emptyTableImage,0,0);
if(this.mouse.over){
    ctx.globalCompositeOperation = "lighter"; 
    ctx.globalAlpha = TABLE.empty.highlightAmount;
    ctx.drawImage(emptyTableImage,0,0);
    ctx.globalAlpha = 1;
    ctx.globalCompositeOperation = "source-over"; 
    
    if(!helpItemsUsed.empty){ // show help is the help action has not yet been done
        drawHelpText(ctx,TABLE.help.empty,TABLE.empty);
    }
    this.cursor = TABLE.empty.cursor;
    if(this.mouse.button & 1){ // bit field
        this.buttonDown = true;
    }else if( this.buttonDown){
        this.active = true;
        setTimeout(addTable,TABLE_REFRESH_DELAY);
        this.buttonDown = false;
        helpItemsUsed.empty = true; // flag this help as not needed as user has complete that task
    }
}else{
    this.cursor = "default";
}
}
function drawTable(){  // darw the table all states
var ctx = this.ctx;
ctx.clearRect(0,0,ctx.canvas.width,ctx.canvas.height);
if(this.active){
    ctx.drawImage(tableImage,0,0);
    if(this.mouse.over){
        if(!this.dragging){ // Dont draw close icon while draggin
            this.drawCloseIcon();
        }
        if(this.mouseOverClose && ! this.dragging){ // if not dragging and mouse over close
            this.cursor = TABLE.closeIcon.cursor;   // set cursor
            if(this.mouse.button & 1){ // bit field  is mouse left down
                this.buttonDown = true;
            }else if(this.buttonDown){ // only close if mouse moves up while over close.
                this.active = false;
                helpItemsUsed.closeTable = true;
                this.buttonDown = false;
                setTimeout(updateTables,TABLE_REFRESH_DELAY);
            }
        }else{ // not over close
            // if near a arrow and mouse button right is down delete the arrow
            if(this.closestArrowIndex > -1 && (this.mouse.button & 4) === 4){ // but field Only button right down
                this.arrows.splice(this.closestArrowIndex,1);
                this.closestArrowIndex = -1;
                this.mouse.button = 0;  // turn mouse click off
                helpItemsUsed.activeArrow = true; // flag arrow delete help as used
            
            }else  // if not near line or close then check for mouse left 
            if(this.mouse.button & 1){ // bit field  if down start dragging new arroe
                if(!this.dragging){ // Start of drag create arrow
                    this.arrows.push({
                        x: this.mouse.x,
                        y: this.mouse.y,
                        xx : this.mouse.x,
                        yy : this.mouse.y,
                    });
                    this.currentArrow = this.arrows[this.arrows.length-1];
                    this.dragging = true;
                    
                }else{   // during drag move arrow endpoint
                    helpItemsUsed.active = true; // flag arrow help as used
                    this.currentArrow.xx = this.mouse.x;
                    this.currentArrow.yy = this.mouse.y;
                }
            }else{ // mouse up
                if(this.dragging){ // is dragging then must be a arrow
                    // if arrow added is smaller than 2 pixels then remove it;
                    if(Math.abs(this.currentArrow.xx-this.currentArrow.x) < TABLE.arrow.minSize && Math.abs(this.currentArrow.y-this.currentArrow.yy) < TABLE.arrow.minSize){
                        this.arrows.length -= 1;
                    }
                    this.currentArrow = null;
                    this.dragging = false;
                }
            }
            this.cursor = TABLE.image.cursor; // set cursor tp table standard
        }
    }  
    if(this.closestArrowIndex > -1 && ! this.dragging){ // is mouse near arrow 
        this.cursor = TABLE.arrow.cursor;  // yes set cursor for arrow
     
    }
    // find arrow closest to mouse
    var minDist = TABLE.arrow.width; // this sets the max distance mouse can be for it to highlight an arrow
    var dist = 0;
    this.closestArrowIndex = -1;
    for(var i = 0; i < this.arrows.length; i++){ // test all arrow
        var a = this.arrows[i];
        drawArrow(ctx,a);  // draw the arrow
        a.highlight = false;  // turn off highlight
        dist = distFromLine(this.mouse.x,this.mouse.y,a.x,a.y,a.xx,a.yy); // get distance from mouse
        if(dist < minDist){  // is closer than any other arrow
            this.closestArrowIndex = i; // yes remember the index
            minDist = dist;
        }
    }
    if(this.closestArrowIndex > -1 && this.mouse.over){ // is a arror close to mouse
        this.arrows[this.closestArrowIndex].highlight = true; // highlight it
    }
    
    ctx.setTransform(1,0,0,1,0,0); // reset transform after arrows drawn
    // show help
    if(this.mouse.over){
        if(this.arrows.length === 0 && !helpItemsUsed.active){
            drawHelpText(ctx,TABLE.help.active,TABLE.image);
        }else
        if(this.closestArrowIndex > -1 && !helpItemsUsed.activeArrow){
            drawHelpText(ctx,TABLE.help.activeArrow,TABLE.image);
        }else
        if(this.closestArrowIndex === -1 && !helpItemsUsed.closeTable){
            drawHelpText(ctx,TABLE.help.closeTable,TABLE.image);
        }
    }
}else{
    this.drawEmpty();
}
}
// renders a table. Stops rendering if the mouse is not over
function tableUpdate(){
if(this.mouse.over){
    this.updating = true;
    requestAnimationFrame(this.update);
}else{
    this.buttonDown = false; // turn of button if dragged off
    this.div.style.cursor = "default";
    this.updating = false;
    this.draw(); // draw another time. This alows for the visual state to be correct
}
this.draw();    
this.div.style.cursor = this.cursor;
}
// Mousecallback starts a table rendering if not allready doing so.
function mouseInOutCallback(){
if(this.mouse.over){
    if(!this.updating){
        this.update();
    }
}else{
    this.div.style.cursor = "default";
}
}
// function to handle mouse events
function mouseEvent(e) {
var m =this; // lazy programer short cut
var t = e.type;
var bounds = m.element.getBoundingClientRect();
m.x = e.clientX - bounds.left;
m.y = e.clientY - bounds.top;
if (t === "mousedown") {
    m.button |= MOUSE.buttonMasks[e.which-1];
} else if (t === "mouseup") { 
    m.button &= MOUSE.buttonMasks[e.which + 2];
} else if (t === "mouseout") { 
    m.button = 0; 
    m.over = false;
    m.table.mouseOver();
} else if (t === "mouseover") { 
    m.over = true;
    m.table.mouseOver();
}
e.preventDefault();
}
// create the mouse inteface for a table
function createMouse(table){
var mouse = {
    x : 0,
    y : 0, 
    over : false,
    table : table,
    element : table.div,
    button : 0,
};
mouse.event = mouseEvent.bind(mouse);
mouse.start = function(){
    MOUSE.events.forEach( n => { this.element.addEventListener(n, this.event); } );
}
mouse.remove = function(){
    MOUSE.events.forEach( n => { this.element.removeEventListener(n, this.event); } );
}
return mouse;
}
function createAddTable(){ // Creates a table. Tables default in inactive
var table = {};
var div = document.createElement("div");
div.style.width =  TABLE.width+ "px";
div.style.height = TABLE.height + "px";
div.style.display = TABLE.DOM.display;
var canvas = document.createElement("canvas");
canvas.width = TABLE.width;
canvas.height = TABLE.height;
canvas.className = TABLE.DOM.tableClass
canvas.style.zIndex = TABLE.DOM.zIndex;
var ctx = canvas.getContext("2d");
table.div = div;
table.canvas = canvas;
table.ctx = ctx;
table.arrows = [];
table.width = TABLE.width;
table.height = TABLE.height;
table.mouseOverClose = false
table.drawCloseIcon = drawClose;
table.draw = drawTable;
table.dragging = false;
table.active = false;
table.update = tableUpdate.bind(table);
table.mouseOver = mouseInOutCallback;  // called by mouseEvent when mouse over out
table.drawEmpty = drawEmpty.bind(table);
table.dead = false; // when removed and not needed it is dead and can then be removed from table array
table.updating = false; // true is animation requests are happening
div.appendChild(canvas); // add canvas
table.mouse = createMouse(table);    
table.draw();
return table;
}
function removeTable(table){ // remove table from dom
table.mouse.remove();  // deactivate moue events
TABLE.tables.removeChild(table.div); // remove from DOM
table.dead = true;// flag as dead to be removed from table array
}
function addTable(){ // Adds a table to table array and DOM
var table = createAddTable();  // create new table
TABLE.tables.appendChild(table.div); // add to the dom
table.mouse.start();  // start the mouse
tableArray.push(table); // add to table array
return table;
}
function updateTables(){ // Updates tables. Removes any dead tables from table array
var closeTables = [];
closeTables = tableArray.filter(t => !t.active);
while(closeTables.length > 1){
    removeTable(closeTables.shift());
}
for(var i = 0; i < tableArray.length; i ++){
    if(tableArray[i].dead){
        tableArray.splice(i,1);
        i -= 1;
    }
}
}
addTable();
body {
    background-color: #982439;
}

#table {
    padding: 10px;
}

canvas {
 position: absolute;
}
<div id="tables">
</div>
Blindman67
  • 51,134
  • 11
  • 73
  • 136
  • Thank you so much, this is exactly what I was looking for. I have some questions, is it ok to ask them here? – Tweakimp Aug 18 '16 at 02:42
  • @Tweakimp Yes all good to ask.. That is what the comments are for. – Blindman67 Aug 18 '16 at 03:40
  • 1) What is this custom cursor url? If I want to use my own cursor, can I just replace it with a link to my root/img/cursorimg.png? 2) The global scale might come in handy when dealing with different screen sizes. Can I detect the users resolution in order to choose the right global scale for him? 3) I'm currently going through the code to understand it and to merge some things I coded in the meantime. Why are you using const? cant I just use var for all variables? – Tweakimp Aug 18 '16 at 03:51
  • @Tweakimp For custom cursor yes a png will work `...style.cursor = "url('cursorDrag.png') 26 25, pointer";` Yes you can use` var` instead of `const` I just do it out of habit (also a coding philosophy re state mutability). You can detect the page size with the window properties `innerWidth` and `innerHeight` It (answer) was thrown together quickly so a little clunky. Its main intention is to show how a "table" object can be constructed to hold everything you need as one object and then easily replicated with event handlers and how to incorporate UI within the graphics styles. – Blindman67 Aug 18 '16 at 04:11
  • @Blindman67 What is the bitwise code doing exactly? Im coding in JSLint and try to get the code clean, and the single '&' gives an "unexpected" error. How can I rewrite these passages with in the "normal" way? :) – Tweakimp Aug 18 '16 at 10:31
  • @Tweaking The `mouse.button` has bits 1, 2, & 3 set for each button left, middle, & right respectively. To make it JSLint friendly create a var to hold the bitwise operation result `var button = (mouse.button & 1) === 1;`, button = (mouse.button & 2) === 2;` and `button = (mouse.button & 4) === 4;` for left, middle, right. then `if(button){` Or add three button flags to the mouse object and set them on mouse down and up events. – Blindman67 Aug 18 '16 at 17:06
  • @Blindman67 Ok thanks, one more. The createAddTable function depends on drawEmpty, drawEmpty depends on addTable and addTable depends on createAddTable. How can this even work? (I think I have even more questions about the code, would it be ok if I contact you via email or something like that? :) – Tweakimp Aug 18 '16 at 18:35
  • @Tweakimp They are all within scope so the is no problem with dependencies. You will see that first table is added as the very last line. That is to ensure all variables are correctly defined. As for email. Sorry No can do, that comes under the heading Consultancy and I am only here to give free advice – Blindman67 Aug 18 '16 at 20:25
0

The problem in your code you are using the static id which is wrong you need to count the tables or div and then you need to pass that count with id.

i resoles this problem check the code:

function drawTable(table) {
    "use strict";
    var draw = table.getContext("2d");
    draw.shadowBlur = 20;
    draw.shadowColor = 'rgba(0,0,0,0.3)';            // shadow
    draw.fillStyle = "#2e3f73";                      // table
    draw.fillRect(35.25,    20,      152.5,  274);
    draw.fillStyle = "#ffffff";                      // lines
    draw.fillRect(111.35,   20,      0.3,    274);   // middle line
    draw.fillRect(35.25,    20,      2,      274);   // lift side
    draw.fillRect(185.75,   20,      2,      274);   // right side
    draw.fillRect(35.25,    20,      152.5,  2);     // top base line
    draw.fillRect(35.25,    292,     152.5,  2);     // bottom base line
    draw.fillRect(20,       156,     183,    2);     // net
}



function addTable() {
   "use strict";
    var container = document.createElement("div"),
  table = document.createElement("canvas"),
  arrowLayer = document.createElement("canvas"),
  width = 223,
  height = 314;
  
  var top_level_div = document.getElementById('tables');
  var count = top_level_div.getElementsByTagName('div').length;
  
 
 container.appendChild(table);
 container.appendChild(arrowLayer);
 container.style.width = width + "px";
 container.style.height = height + "px";
 container.style.display = "inline-block";
    document.getElementById("tables").appendChild(container);
 
 table.width = width;
    table.height = height;
    table.className = "table";
 table.style.zIndex = "0";
 drawTable(table);
 
 arrowLayer.width = width;
    arrowLayer.height = height;
 arrowLayer.className = "arrow";
    arrowLayer.style.zIndex = "1";
 arrowLayer.id = "arrow1"+count;
  
  
  
}



function removeTable() {
    "use strict";
    var child = document.getElementById("tables").lastChild;
    child.parentNode.removeChild(child);
}

function drawArrow(start_x, start_y, end_x, end_y) {
    "use strict";
  var top_level_div = document.getElementById('tables');
  var count = top_level_div.getElementsByTagName('div').length;
    //alert(count);
  count = count-1;
    var draw = document.getElementById('arrow1'+count).getContext('2d'),
  angle = Math.atan((end_y - start_y) / (end_x - start_x)),
  length = Math.sqrt(Math.pow((end_x - start_x), 2) + Math.pow((end_y - start_y), 2));
 
 // set colors and style
  draw.strokeStyle = "#ffb900";
 draw.fillStyle = "#ffb900";
 draw.lineWidth = 9;
  
 // draw arrow line
    draw.beginPath();
   draw.translate(start_x, start_y);
    draw.moveTo(0, 0);
    draw.rotate(angle);
    draw.lineTo(length - 23, 0); // note: arrowhead is 24px long and total arrow is line+head 
    draw.stroke();
   draw.moveTo(-start_x, -start_y);
 
 // draw arrow head
 draw.beginPath();
 draw.moveTo(length, 0);
 draw.lineTo(length - 24, -7.5); // ^ see note above
 draw.lineTo(length - 24, 7.5);
 draw.fill();
 
 //reset context
    draw.rotate(-angle);
    draw.moveTo(-start_x, -start_y);
    draw.translate(-start_x, -start_y);
}
body {
    background-color: #982439;
}

#table {
    padding: 10px;
}

canvas {
 position: absolute;
}
<button onclick="addTable()">Add table</button>
<button onclick="removeTable()">Remove table</button>
<button onclick="drawArrow(42, 25, 186, 293)">Draw arrow</button>
<button onclick="drawArrow(41, 290, 186, 20)">Draw arrow2</button>
</br>
<div id="tables">
</div>

Hope this code will help you

Yaseen Ahmad
  • 1,807
  • 5
  • 25
  • 43
  • Sorry, but neither does this allow me to draw arrows with click-and-drag nor delete them with right-click on them. – Tweakimp Aug 17 '16 at 20:02