Search code examples
javascripthtmlcanvaskineticjs

KineticJS - Swapping shape positions upon contact/mousemove trigger


I'm using KineticJS and trying to get something done that seems simple enough: trying to get a shape to change position with the shape that's currently being dragged.

The idea is that:
• You pick up a shape on the canvas. This triggers a mousedown event listener, which saves the current position of the shape you've picked up.
• While holding the shape, if you trigger the mouseover on another shape, that shape's event gets triggered and swaps its position, based on the current shape's saved position.

Here is the relevant code I've written to try to get that working:

Board setup: Simply just setting up the board and calling the needed functions here. I'm not doing anything with the stage, gameBoard, or ctx yet (part of it was for when I tried to get drawImage to work on multiple canvases, but have abandoned that for now).

class BoardView {
  constructor(stage, gameBoard, ctx) {
    this.stage = stage;
    this.gameBoard = gameBoard;
    this.ctx = ctx;
    this.orbs = [[], [], [], [], []];
    this.setupBoard();
  }

Board setup functions: This is where I go about setting up the board and giving each Kinetic Circle the attributes it needs to render on the Layer. Maybe I'm missing something obvious here?

  setupBoard () {
    for (let colIdx = 0; colIdx < 5; colIdx++) {
      this.addRow(colIdx);
    }

    this.renderBoard();
  }

  addRow (colIdx) {
    for (let rowIdx = 0; rowIdx < 6; rowIdx++) {
      let orbType = Math.round(Math.random() * 5);
      let orbColor;

      if (orbType === 0) {
          orbColor = "#990000";
        } else if (orbType === 1) {
          orbColor = "#112288";
        } else if (orbType === 2) {
          orbColor = "#005544";
        } else if (orbType === 3) {
          orbColor = "#776611";
        } else if (orbType === 4) {
          orbColor = "#772299";
        } else {
          orbColor = "#dd2277";
      }
      let orbject = new Kinetic.Circle({
        x: (rowIdx + 0.5) * 100, 
        y: (colIdx + 0.5) * 100,
        width: 100, 
        height: 100,
        fill: orbColor, 
        draggable: true
      });
      this.orbs[colIdx].push(orbject);
    }
  }

Board render function: This is where I add all the Kinetic Circle objects into new layers, and give those layers all its own attributes to work with when I call the event handlers. I also set up the event handlers here after adding the layers to the stage. Am I perhaps messing this up by adding too many layers?

  renderBoard () {

    for (let row = 0; row < this.orbs.length; row++) {
      for (let orb = 0; orb < this.orbs[row].length; orb++) {
        let layer = new Kinetic.Layer();

        layer.add(this.orbs[row][orb]);
        layer.moving = false;
        layer.orbId = `orb${row}${orb}`;
        layer.pos = [this.orbs[row][orb].attrs.x, this.orbs[row][orb].attrs.y];

        this.stage.add(layer);

        layer.on("mousedown", this.handleMouseDown);
        layer.on("mouseup", this.handleMouseUp);
        layer.on("mouseout", this.handleMouseOut);
        layer.on("mousemove", this.handleMouseMove);
      }
    }
  }

Mouse event handlers: This is where I think I'm having my main problem. How I handle the event of moving the mouse to change orbs, perhaps I'm doing something terribly wrong?

  handleMouseDown (e) {
    window.currentOrb = this;
    console.log(window.currentOrb.orbId);

    this.moving = true;
  }

  //handleMouseUp (e) {
  //  window.currentOrb = undefined;
  //  this.moving = false;
  //}

  //handleMouseOut (e) {
  //}

  handleMouseMove (e) {
    if (window.currentOrb !== undefined && window.currentOrb.orbId != this.orbId) {
      this.children[0].attrs.x = window.currentOrb.pos[0];
      this.children[0].attrs.y = window.currentOrb.pos[1];
      this.children[0].draw();
    } else {
    }
  }
}


module.exports = BoardView;

I've tried looking at the KineticJS docs and many StackOverflow answers as I could in hopes of finding a solution that would work for me, but nothing I've seen and tried so far (including the suggestions that came up as I wrote this question) seemed to be of help, and I'm aware the way I've gone about this so far is probably far from the best way to accomplish what I want, so I'm open to any suggestions, pointers, answered questions, or whatever can point me in the right direction to what I'm missing in order to get this to work.

In case this is helpful, here is also a visualization of how things look when the board is rendered.

enter image description here

The circles are all Kinetic Circles (orbs for the purpose of what I'm going for), and clicking and dragging one to another, the one that isn't being dragged but hovered over should move to the original position of the dragged circles.

Thanks!

EDIT:

I made a few changes to the code since then. First off, I changed adding many layers to the stage, to just one:

renderBoard () {
  let layer = new Kinetic.Layer();

  for (let row = 0; row < this.orbs.length; row++) {
    for (let orb = 0; orb < this.orbs[row].length; orb++) {

      layer.add(this.orbs[row][orb]);

      this.orbCanvases.push(orbCanvas.id);
    }
  }
  this.stage.add(layer);
} 

I instead added listeners to the orb objects instead:

addRow (colIdx) {
  for (let rowIdx = 0; rowIdx < 6; rowIdx++) {

    //same code as before

    let orbject = new Kinetic.Circle({
      x: (rowIdx + 0.5) * 100, y: (colIdx + 0.5) * 100,
      width: 100, height: 100,
      fill: orbColor, draggable: true, pos: [rowIdx, colIdx]
    });
    orbject.on("mousedown", this.handleMouseDown);
    orbject.on("mouseup", this.handleMouseUp);
    orbject.on("mouseout", this.handleMouseOut);
    orbject.on("mousemove", this.handleMouseMove);

    this.orbs[colIdx].push(orbject);
  }
}

This has had the benefit of making drag and drop much much faster where before, it was going very slow, but I still can't get my objects to swap position.

To be clear, my main issue is knowing which x, y values I should be changing. At the moment in handleMouseMove, I've been trying to change:

e.target.attrs.x = newX;
e.target.attrs.y = newY;
// newX and newY can be any number

However, no matter what I change it to, it has no effect. So it would help me to know if I'm changing the wrong thing/place, for example, maybe I'm supposed to be changing the Kinetic Circle from the array I've stored? Thanks again.

EDIT 2:

I think I got it! However, I had to take this.orbs and set it in the window at window.orbs, and to test it I did:

window.orbs[0][0].x(450);
window.orbs[0][0].draw();

And this caused the x position to change. But putting it in a window does not seem like good practice?

EDIT 3:

I got the orbs to now swap continuously, except for when it's the same orb being swapped again while mouseover is continuing to fire. At mouseup however, it can be swapped again. I also had to set up multiple layers again to get the mouseover events to work while holding another orb, but the performance seems to have improved a little.

I'm going to try and figure out how to get them to be able to swap continuously on the same mouse hold, but in the meantime, here is the code I wrote to achieve this:

addRow (colIdx) {
  for (let rowIdx = 0; rowIdx < 6; rowIdx++) {

    // same code as before, changed attr given to Kinetic.Circle

    let orbject = new Kinetic.Circle({
      x: (rowIdx + 0.5) * 100, y: (colIdx + 0.5) * 100,
      width: 100, height: 100,
      fill: orbColor, draggable: true, orbId: `orb${colIdx}${rowIdx}`
    });
  }
}

handleMouseDown (e) {
  window.currentOrb = window.orbs[e.target.attrs.orbId];
  window.newX = e.target.attrs.x;
  window.newY = e.target.attrs.y;
}

Mouse down saving currentOrb by ID and its X and Y

handleMouseUp (e) {
  window.currentOrb.x(window.newX);
  window.currentOrb.y(window.newY);
  window.currentOrb.parent.clear();
  window.currentOrb.parent.draw();
  window.currentOrb.draw();
  window.currentOrb = undefined;
  for (let i = 0; i < 5; i++) {
    for (let j = 0; j < 6; j++) {
      window.orbs[`orb${i}${j}`].draw();
    }
  }
}

When mouse is released, currently, all orbs are redrawn so they can all be used. I plan to refactor this so only orbs hovered over have this change.

handleMouseMove (e) {

  if (window.currentOrb !== undefined && (window.currentOrb.attrs.orbId !== e.target.attrs.orbId)) {
    window.orbMove.pause();
    window.currentTime = 0;
    window.orbMove.play();
    let targOrbX = e.target.attrs.x;
    let targOrbY = e.target.attrs.y;
    // This is the target orb that's being changed's value
    // We're storing this in targOrb

    e.target.x(window.newX);
    e.target.y(window.newY);
    e.target.parent.clear();
    e.target.parent.draw();
    e.target.draw();

    // Here we have the previously set current orb's position becoming
    // the target orb's position

    window.newX = targOrbX;
    window.newY = targOrbY;

    // Now that the move is made, we can set the newX and Y to be the
    // target orb's position once mouseup
  }
}

Orb swapping logic which works for passing over orbs once, but not if they are passed over again in the same turn.


Solution

  • When does the "hover" officially take place?

    • When the mouse event's position enters the second orb? If yes, hit test the mouse vs every non-dragging orb:

      // pseudo-code -- make this test for every non-dragging orb
      var dx=mouseX-orb[n].x;
      var dy=mouseY-orb[n].y; 
      if(dx*dx+dy*dy<orb[n].radius){
          // change orb[n]'s x,y to the dragging orb's x,y (and optionally re-render)
      }
      
    • When the dragging orb intersects the second orb? If yes, collision test the dragging orb vs every non-dragging orb:

      // pseudo-code -- make this test for every non-dragging orb
      var dx=orb[draggingIndex].x-orb[n].x;
      var dy=orb[draggingIndex].y-orb[n].y;
      var rSum=orb[draggingIndex].radius+orb[n].radius;
      if(dx*dx+dy*dy<=rSum*rSum){
          // change orb[n]'s x,y to the dragging orb's x,y (and optionally re-render)
      }
      

    BTW, if you drag an orb over all other orbs, the other orbs will all stack on the dragging orbs original position -- is that what you want?