Dragging shapes from a palette onto a Konvajs stag

2019-06-02 09:49发布

问题:

On the Konvajs chat stream someone recently asked for an example of drag-and-drop from a palette onto an HTML5 canvas fronted by the Konvajs library. There were no ready examples and I was curious about how to achieve it.

I answered the question in a codepen but decided to post here for (my own) future reference. See my answer below.

回答1:

Here is my solution using jquery UI draggable & droppables. Konvajs requires jquery so the use of jquery UI is only a small step further. The palette is a set of small canvas elements with one shape drawn per draggable item. The palette can be housed on any html element and does not need to be attached to the main stage in any way.

// Set up the canvas to catch the dragged shapes
var s1 = new Konva.Stage({container: 'container1', width: 500, height: 200});
// add a layer to host the 'dropped' shapes.
var layer1 = new Konva.Layer({draggable: false});
s1.add(layer1);

// set up the palette of draggable shapes - 5 sample shapes.
var palletteEle = $('#pallette');
var d, ps, l, c;
for (var i = 0; i<5; i = i + 1){
  // make a div to hold the shape
  d = $('<div id="shape' + i + '" class="draggable">Shape</div>')
  palletteEle.append(d)

  // make a mini stage to hold the shape
  ps = new Konva.Stage({container: 'shape' + i, width: 50, height: 50});

  // make a layer to hold the shape
  l = new Konva.Layer();

  // add layer to palette
  ps.add(l);

  // make a shape - red circles for example
  c = new Konva.Circle({x: 24, y: 24, radius: 22, fill: 'red', stroke: 'black'})    
  l.add(c);
  ps.draw();
}

// make a crosshair to give some idea of the drop location
var cross = new Konva.Line({points: [10, 0, 10, 20, 10, 10, 0, 10, 20, 10],
      stroke: 'gold',
      strokeWidth: 1,
      lineCap: 'round',
      lineJoin: 'round'})
layer1.add(cross);
//s1.draw();

// make the main stage a drop target
$('#container1').addClass('droppable');

// function to move the cross hairs
function moveCross(x, y){
  cross.x(x);
  y = y - $('#container1').offset().top;
  cross.y(y < 0 ? 0 : y);
  s1.draw();
}


// draggable setup. Movecross used to move the crosshairs. More work needed but shows the way. 
$( ".draggable" ).draggable({
    zIndex: 100, 
    helper: "clone", 
    opacity: 0.35,
    drag: function( event, ui ) {moveCross(ui.offset.left  , ui.offset.top + $(this).offset().top)}
});

// set up the droppable
$( ".droppable" ).droppable({
  drop: function( event, ui ) {
    dropShape(ui.position.left, ui.position.top)
  }
});

//  Function to create a new shape when we drop something dragged from the palette
function dropShape() {  
  var c1 = new Konva.Circle({x: cross.x(), y: cross.y(), radius: 22, fill: 'red', stroke: 'black'});
  layer1.add(c1);
  cross.x(0); cross.y(0);
  cross.moveToTop(); // move the cross to the top to stop going bahind previously dropped shapes.
  s1.draw();
}
p
{
  padding: 4px;
  
}
#container1
{
  display: inline-block;
  width: 500px; 
  height: 200px; 
  background-color: silver;
  overflow: hidden; 
}
#pallette
{
 height: 52px; width: 500px; 
 border: 1px solid #666;
  margin-bottom: 10px;
  z-index: 10;
}
.draggable
{
  width:50px;
  height: 50px;
  display: inline-block;
  border: 1px solid #666;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js"></script><link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
<script src="https://cdn.rawgit.com/konvajs/konva/1.6.5/konva.min.js"></script>
<p>Drag a red circle from the pallette and drop it on the grey canvas.
</p>
<div id='pallette'></div>
<div id='container1'></div>



回答2:

I tried Vanquished Wombat's solution, it was a great example. But ultimately I wanted my palette to be separate from Konva. So I modified that original snippet to work with Html5 drag & drop, without any jQuery. See the snippet below. You can drag stars & circles from the palette into the Konva canvas. Currently you have to drop onto another shape, but you can modify it easily to drop anywhere on the canvas. I'm using text for the palette items and a custom image for the drag object just for fun. But you can just use an img instead of using the setDragImage code.

const CUSTOM_DATA_TYPE = 'text/x-node-type';

// Set up the canvas to catch the dragged shapes
var s1 = new Konva.Stage({
  container: 'container1',
  width: 500,
  height: 200
});
// add a layer to host the 'dropped' shapes.
var layer1 = new Konva.Layer({
  draggable: false
});
s1.add(layer1);

for (let t = 0; t < 10; t++) {
  let rect = document.getElementById('container1').getBoundingClientRect();
  let x = Math.floor(Math.random() * rect.width);
  let y = Math.floor(Math.random() * rect.height);
  let type = Math.floor(Math.random() * 100) % 2 == 0 ? 'circle' : 'star';
  dropShape(x, y, type);
}

//  Function to create a new shape when we drop something dragged from the palette
function dropShape(x, y, type) {
  var shape;

  if (type == 'circle') {
    shape = new Konva.Circle({
      x: x,
      y: y,
      radius: 22,
      fill: 'blue',
      stroke: 'black'
    });
  } else {
    shape = new Konva.Star({
      x: x,
      y: y,
      numPoints: 5,
      innerRadius: 10,
      outerRadius: 20,
      fill: 'purple',
      stroke: 'black'
    });
  }
  layer1.add(shape);
  s1.draw();
}

function cursorToCanvasPos(e) {
  let clientRect = document.getElementById('container1').getBoundingClientRect();
  let pointerPosition = {
    x: e.clientX - clientRect.x,
    y: e.clientY - clientRect.y,
  };
  return pointerPosition;
}

function getHoveredShape(e) {
  let pointerPosition = cursorToCanvasPos(e);

  return s1.getIntersection(pointerPosition);
}

function onDragStart(e, type) {
  // Do this or other things can mess with your drag
  e.stopPropagation();

  e.dataTransfer.setData(CUSTOM_DATA_TYPE, type);

  e.dataTransfer.effectAllowed = "all";

  var dragIcon = document.createElement('img');
  dragIcon.src = 'https://placehold.it/100x100';
  dragIcon.width = 100;
  e.dataTransfer.setDragImage(dragIcon, 150, 150);
}

function onDragOver(e) {
  // Might break if you don't have this
  e.stopPropagation();
  // Breaks for sure if you don't have this
  e.preventDefault();

  let thing = getHoveredShape(e);

  if (thing) {
    e.dataTransfer.dropEffect = "move";
    // Just fire off a custom even if you want to, this does nothing in this example.
    thing.fire('htmlDragOver');
  } else {
    e.dataTransfer.dropEffect = "none";
  }
}

function onDrop(e) {
  e.stopPropagation();

  let type = e.dataTransfer.getData(CUSTOM_DATA_TYPE);
  let pos = cursorToCanvasPos(e);

  dropShape(pos.x, pos.y, type);
}
p {
  padding: 4px;
}

#container1 {
  display: inline-block;
  width: 500px;
  height: 200px;
  background-color: silver;
  overflow: hidden;
}

#palette {
  height: 52px;
  width: 500px;
  border: 1px solid #666;
  margin-bottom: 10px;
  z-index: 10;
}

#palette span  {
  width: 50px;
  height: 25px;
  display: inline-block;
  border: 1px solid #666;
}
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
<script src="https://cdn.rawgit.com/konvajs/konva/1.6.5/konva.min.js"></script>


<p>Drag circle/star from the palette onto an existing shape on the canvas below.
</p>
<div id='palette'>
  <!-- Pre-load this image so it'll be used for our drag -->
  <img src="https://placehold.it/100x100" style="display: none">

  <span draggable="true" ondragstart="onDragStart(event, 'circle')">circle</span>
  <span draggable="true" ondragstart="onDragStart(event, 'star')">star</span>
</div>
<div id='container1' ondragover="onDragOver(event)" ondrop="onDrop(event)"></div>