1

I'm trying to use a mousemove event on a canvas, inspired from Wayne's answer here, and the corresponding fiddle.

The problem is, I'm working with a code which creates "layered" canvas in the following way:

  // canvas data layers
  ["marks", "foreground", "brushed", "highlight", "clickable_colors"].forEach(function(layer, i) {
    canvas[layer] = selection
      .append("canvas")
      .attr({
      id: layer, //added an id for easier selecting for mouse event 
      class: layer,
      style: "z-index: " + i +10000000
      })[0][0];
    ctx[layer] = canvas[layer].getContext("2d");
  });

My goal is to get the color which is on the layer "clickable_colors", and so I adapted the fiddle's script to set the mousemove event on that layer:

var my_clickable_canvas = document.getElementById('clickable_colors');
var context = my_clickable_canvas.getContext('2d');

context.fillStyle = "rgb(255,0,0)";
context.fillRect(0, 0, 50, 50);
context.fillStyle = "rgb(0,0,255)";
context.fillRect(55, 0, 50, 50);

$("#clickable_colors").mousemove(function(e) {
    debugger;
    var pos = findPos(this);
    var x = e.pageX - pos.x;
    console.log(x)
    var y = e.pageY - pos.y;
    var coord = "x=" + x + ", y=" + y;
    var c = this.getContext('2d');
    var p = c.getImageData(x, y, 1, 1).data; 
    var hex = "#" + ("000000" + rgbToHex(p[0], p[1], p[2])).slice(-6);
    $('#examplecanvas').html(coord + "<br>" + hex);
    console.log("hi")
});

However, the mousemove event isn't triggered at all. I first assumed it had to do with z-index, but giving it the highest value to make sure it's on top didn't solve it.

Does anyone know what might be preventing the event from firing? Or what I should pay attention to, given the "layered" formulation of canvases? Any help appreciated!

EDIT: Another explanation I found is that the "layered" canvases belong to a parent div element, whereas the working fiddle provides a canvas directly in the body. Could this be preventing the mouse event from working? If so, how to cope with canvases in a div?

enter image description here

EDIT2: In response to a comment, a simplified and verifiable example is provided in this bl.ocks. The original code is available here (cf. example file brushing.html). The question is also discussed on the corresponding issue board.

sc28
  • 1,163
  • 4
  • 26
  • 48
  • 1
    Your code has 2528 lines. It's unrealistic expecting an answer in this situation. Please, provide a [MCVE]. – Gerardo Furtado Jul 05 '17 at 11:19
  • 1
    Add the mouse event handler to the document rather than the canvases. Keep track of the active layer and handle the mouse appropriately. This means you only have to use one mouse event handler rather than a complex mix of event handlers with bubbling and capture. – Blindman67 Jul 05 '17 at 12:59
  • @GerardoFurtado : unfortunately I'm working on an existing code, which as a novice I don't understand fully. It is difficult therefore to provide a minimal working example that might capture the issue. I was rather hoping for some generic help, in case I was doing anything wrong. – sc28 Jul 05 '17 at 14:24
  • @GerardoFurtado : I finally made a simplified example, which I posted on the github [issue board](https://github.com/syntagmatic/parallel-coordinates/issues/305). The issue can be observed in the following [bl.ocks](http://bl.ocks.org/sc28/f978a48814605b527b38c9e03786dea5), and the code is available on [github](https://github.com/sc28/parallel-coordinates.git). Hope this makes things clearer, any help still much welcome! – sc28 Aug 03 '17 at 16:51

1 Answers1

2

Multy layered canvas mouse events

The best way to approch is the simplest. Create just one mouse handler that listens to document mouse events. Then keep track of the active layer and handle it appropreatly.

The snippet is taken from another answer and adapted to create lines on several layers. Click the layer to select it, then click drag to add lines. Each layer has its own colour and needs to be selected to modify it.

There is only one mouse handler that listens to the document mouse events, the reset is done in the render code.

const ctx1 = canvas1.getContext("2d");
const ctx2 = canvas2.getContext("2d");
const ctx3 = canvas3.getContext("2d");
const ctx4 = canvas4.getContext("2d");

const Point2 = (x,y) => ({x,y});  // creates a point
const Line = (p1,p2) => ({p1,p2});
const setStyle = (style,ctx) => eachOf(Object.keys(style), key => { ctx[key] = style[key] } );
const eachOf = (array, callback) => {var i = 0; while (i < array.length && callback(array[i],i ++) !== true ); };


const list = {
    items : null,
    add(item) { this.items.push(item); return item },
    eachItem(callback) { 
        var i = 0;
        while(i < this.items.length){
             callback(this.items[i],i++);
        }
    }
}
function createList(extend){
    return Object.assign({},list,{items : []},extend);
}
// this will extend the points list
function getClosestPoint(from ,minDist) {
    var closestPoint;
    this.eachItem(point => {
        const dist = Math.hypot(from.x - point.x, from.y - point.y);
        if(dist < minDist){
            closestPoint = point;
            minDist = dist;
        }
    });
    return closestPoint;
}
function distanceLineFromPoint(line,point,points){
    const lx = points.items[line.p1].x;
    const ly = points.items[line.p1].y;
    const v1x = points.items[line.p2].x - lx;
    const v1y = points.items[line.p2].y - ly;
    const v2x = point.x - lx;
    const v2y = point.y - ly;
    // get unit dist of closest point
    const u = (v2x * v1x + v2y * v1y)/(v1y * v1y + v1x * v1x);
    if(u >= 0 && u <= 1){  // is the point on the line
        return Math.hypot(lx + v1x * u - point.x, ly + v1y * u - point.y);
    } else if ( u < 0 ) {  // point is before start
        return Math.hypot(lx - point.x, ly - point.y);
    }
    // point is after end of line
    return Math.hypot(points.items[line.p2].x - point.x, points.items[line.p2].y - point.y);
}
// this will extend the lines list
function getClosestline(from ,minDist) {
    var closestLine;
    this.eachItem(line => {
        const dist = distanceLineFromPoint(line,from,this.points);
        if(dist < minDist){
            closestLine = line;
            minDist = dist;
        }
    });
    return closestLine;
}
function drawPoint(point,ctx){
    ctx.moveTo(point.x,point.y);
    ctx.rect(point.x - 2,point.y - 2, 4,4);
}
function drawLine(line,ctx,points){
    ctx.moveTo(points.items[line.p1].x,points.items[line.p1].y);
    ctx.lineTo(points.items[line.p2].x,points.items[line.p2].y);
}
function drawLines(ctx){ this.eachItem(line => drawLine(line,ctx,this.points)) }
function drawPoints(ctx){this.eachItem(point => drawPoint(point,ctx)) }

const mouse  = {x : 0, y : 0, button : false, drag : false, dragStart : false, dragEnd : false, dragStartX : 0, dragStartY : 0}
function mouseEvents(e){
 mouse.x = e.pageX;
 mouse.y = e.pageY;
 const lb = mouse.button;
 mouse.button = e.type === "mousedown" ? true : e.type === "mouseup" ? false : mouse.button;
 if(lb !== mouse.button){
  if(mouse.button){
   mouse.drag = true;
   mouse.dragStart = true;
   mouse.dragStartX = mouse.x;
   mouse.dragStartY = mouse.y;
  }else{
   mouse.drag = false;
   mouse.dragEnd = true;
  }
 }
}
["down","up","move"].forEach(name => document.addEventListener("mouse" + name, mouseEvents));
// short cut vars 
var w;
var h;
var cw;  // center 
var ch;
var globalTime;
var closestLine;
var closestPoint;
var pointDrag; // true is dragging a point else dragging a line
var dragOffsetX;
var dragOffsetY;
var cursor;
var toolTip;
var helpCount = 0;
const minDist = 20;
var points ;
var lines;
const layers = [{
    lineStyle : { lineWidth : 2, strokeStyle : "green", fillStyle : "Green" },
    pointStyle : { lineWidth : 1, strokeStyle : "green", fillStyle : "Green"},
    font : { fillStyle : "green", font : "18px arial", textAlign : "left"},
    context : ctx1,
    canvas : canvas1,
    points :(points =  createList({ getClosest : getClosestPoint,draw : drawPoints})),
    lines : createList({getClosest : getClosestline, draw : drawLines, points : points }),
    ready : false,
  },{
    lineStyle : { lineWidth : 2, strokeStyle : "blue", fillStyle : "blue" },
    pointStyle : { lineWidth : 1, strokeStyle : "blue", fillStyle : "blue"},
    font : { fillStyle : "blue", font : "18px arial", textAlign : "left"},
    context : ctx2,
    canvas : canvas2,
    points :(points =   createList({ getClosest : getClosestPoint,draw : drawPoints})),
    lines : createList({getClosest : getClosestline, draw : drawLines, points : points}),
    ready : false,
  },{
    lineStyle : { lineWidth : 2, strokeStyle : "gold", fillStyle : "gold" },
    pointStyle : { lineWidth : 1, strokeStyle : "gold", fillStyle : "gold"},
    font : { fillStyle : "gold", font : "18px arial", textAlign : "left"},
    context : ctx3,
    canvas : canvas3,
    points :(points =   createList({ getClosest : getClosestPoint,draw : drawPoints})),
    lines : createList({getClosest : getClosestline, draw : drawLines, points : points}),
    ready : false,
  },{
    lineStyle : { lineWidth : 2, strokeStyle : "aqua", fillStyle : "aqua" },
    pointStyle : { lineWidth : 1, strokeStyle : "aqua", fillStyle : "aqua"},
    font : { fillStyle : "aqua", font : "18px arial", textAlign : "left"},
    context : ctx4,
    canvas : canvas4,
    points : (points =  createList({ getClosest : getClosestPoint,draw : drawPoints})),
    lines : createList({getClosest : getClosestline, draw : drawLines, points : points}),
    ready : false,
  }
];
var currentLayer = 0;
const highlightStyle = {
  lineWidth : 3,
  strokeStyle : "red",
}
const font = {
  font : "18px arial",
  fillStyle : "black",
  textAlign : "center",
}


// main update function
function update(timer){
  if(mouse.button){
    if(mouse.x < 50 && mouse.y < 28 *5){
        mouse.drag=mouse.button=mouse.dragStart = false;
        currentLayer = (mouse.y / 28)|0;
        eachOf(layers,layer=>layer.ready = false);
    }
  }
    const layer = layers[currentLayer];
    const ctx = layer.context;
    const canvas = layer.canvas;
    const lines = layer.lines;
    const points = layer.points;
    const lineStyle = layer.lineStyle;
    const pointStyle = layer.pointStyle;
    cursor = "crosshair";
    toolTip = helpCount < 2 ? "Click drag to create a line" : "";
    globalTime = timer;
    ctx.setTransform(1,0,0,1,0,0); // reset transform
    ctx.globalAlpha = 1;           // reset alpha
 if(w !== innerWidth || h !== innerHeight){
  cw = (w = canvas1.width = innerWidth) / 2;
  ch = (h = canvas1.height = innerHeight) / 2;
    canvas2.height = canvas3.height = canvas4.height = h;
    canvas2.width = canvas3.width = canvas4.width = w;
    eachOf(layers,layer=>layer.ready = false);
 }else{
  ctx.clearRect(0,0,w,h);
 }
  if(mouse.drag=== false){
    if(mouse.x < 50 && mouse.y < 28 *5){
      cursor = "pointer";
      toolTip = "Click to select layer";
      closestPoint = closestLine = undefined;
    }else{

      closestLine = undefined;
      closestPoint = points.getClosest(mouse,minDist);
      if(closestPoint === undefined){
         closestLine = lines.getClosest(mouse,minDist);
      }
      if(closestPoint || closestLine){
         toolTip = "Click drag to move " + (closestPoint ? "point" : "line");     
         cursor = "move";
      }
    }
  }
  if(mouse.dragStart){
    if(closestPoint){
      dragOffsetX = closestPoint.x - mouse.x;
      dragOffsetY =  closestPoint.y - mouse.y;
      pointDrag = true;
    
    }else if( closestLine){
      dragOffsetX = points.items[closestLine.p1].x - mouse.x;
      dragOffsetY = points.items[closestLine.p1].y - mouse.y;
      pointDrag = false;
    
    } else {
      points.add(Point2(mouse.x,mouse.y));
      closestPoint = points.add(Point2(mouse.x,mouse.y));
      closestLine = lines.add(Line(points.items.length-2,points.items.length-1));
      dragOffsetX = 0;
      dragOffsetY = 0;
      pointDrag = true;
      helpCount += 1;
      
    }
    mouse.dragStart = false;
  
  }else if(mouse.drag){
      cursor = 'none';
      if(pointDrag){
        closestPoint.x = mouse.x + dragOffsetX;
        closestPoint.y = mouse.y + dragOffsetY;
      }else{
        const dx = mouse.x- mouse.dragStartX;
        const dy = mouse.y -mouse.dragStartY;
        mouse.dragStartX = mouse.x;
        mouse.dragStartY = mouse.y;
        points.items[closestLine.p1].x +=  dx;
        points.items[closestLine.p1].y +=  dy;
        points.items[closestLine.p2].x +=  dx;
        points.items[closestLine.p2].y +=  dy;        
      }
  }else{
  
  
  }
  // draw all points and lines
  setStyle(lineStyle,ctx);
  ctx.beginPath();
  lines.draw(ctx);
  ctx.stroke();
  setStyle(pointStyle,ctx);
  ctx.beginPath();
  points.draw(ctx);
  ctx.stroke();

  
  // draw highlighted point or line
  setStyle(highlightStyle,ctx);
  ctx.beginPath();
  if(closestLine){ drawLine(closestLine,ctx,points) }
  if(closestPoint){ drawPoint(closestPoint, ctx) }
  
  ctx.stroke();
  eachOf(layers,(layer,i)=>{
    if(!layer.ready){
        const ctx = layer.context;
        ctx.globalAlpha = 0.75;
        ctx.clearRect(0,0,w,h);
        setStyle(layer.lineStyle,ctx);
        ctx.beginPath();
        layer.lines.draw(ctx);
        ctx.stroke();
        setStyle(layer.pointStyle,ctx);
        ctx.beginPath();
        layer.points.draw(ctx);
        ctx.stroke();
        setStyle(layer.font,ctx);
        ctx.fillText("Layer " + (i + 1), 10, i * 28+28);
        layer.ready = true;
        ctx.globalAlpha = 1;
     }
   });
    
  setStyle(layer.font,ctx);  
  ctx.fillText("Layer On" , 10,currentLayer * 28+28);
  
  if(helpCount < 2){
     setStyle(font,ctx);
     ctx.fillText(toolTip,cw,30);
  }
  
  
  canvas4.style.cursor = cursor;
  if(helpCount < 5){
      canvas4.title = toolTip;
  }else{
      canvas4.title = "Click layer to select it";
  }
  requestAnimationFrame(update);
}
requestAnimationFrame(update);
canvas { 
  position : absolute; 
  top : 0px; 
  left : 0px; 
}
<canvas id="canvas1"></canvas>
<canvas id="canvas2"></canvas>
<canvas id="canvas3"></canvas>
<canvas id="canvas4"></canvas>
Blindman67
  • 51,134
  • 11
  • 73
  • 136
  • Thanks, appreciate the help. However, adding the mouse event to the `$(document)` didn't help, as I believe some other functions require that the event is on a canvas specifically. Also I'm not too sure what you mean by "keep track and handle the active layer appropriately". Would this have to do with the nested structure of the HTML/canvas, preventing the canvases to become active? Would you mind responding to my EDIT in original post (cf. screenshot)? – sc28 Jul 05 '17 at 14:27
  • 1
    @sc28 The only other option is to add mouse events to each canvas and then depending on which layer is active you need to pass the event on to each canvas. with only the active canvas taking action on the event. The events will bubble up from the lowest canvas. You can set event capture to true on the canvas you want to get the events then you can have the current active lay get the event first. Best you read up on events https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Building_blocks/Events – Blindman67 Jul 05 '17 at 15:21