简体   繁体   中英

Can I use an image (.svg) or svg path(?) in Konva.Group()'s clipFunc?

Suppose I have a map of the world.

And I'd like each continent to be an area where I could attach shapes to and drag/reshape them, while always being clipped by the continent's shape borders/limits.

Here's what I have so far:

 const stage = new Konva.Stage({ container: 'stage', width: window.innerWidth, height: window.innerHeight }); const layer = new Konva.Layer(); const group = new Konva.Group({ clipFunc: function (ctx) { ctx.arc(250, 120, 50, 0, Math.PI * 2, false); ctx.arc(150, 120, 60, 0, Math.PI * 2, false); }, }); const shape = new Konva.Rect({ x: 150, y: 70, width: 100, height: 50, fill: "green", stroke: "black", strokeWidth: 4, draggable: true, }); group.add(shape); layer.add(group); stage.add(layer);
 body { margin: 0; padding: 0; }
 <script src="https://cdnjs.cloudflare.com/ajax/libs/konva/8.4.0/konva.min.js"></script> <div id="stage"></div>

My question is, how could I use the clipFunc to draw a continent's limits? Could I use and image? svg path? I can't seem to find the answer in the docs .

To use an image, you can use the drawImage method of the canvas context in the clipFunc

const image = new Image();
image.src = 'image.png';

const group = new Konva.Group({
    clipFunc: function (ctx) {
        ctx.drawImage(image, 0, 0);
    },
});

To use an SVG path, you can use the clip method of the canvas context in the clipFunc

const group = new Konva.Group({
    clipFunc: function (ctx) {
        ctx.clip('M10,10 h80 v80 h-80 Z');
    },
});

TLDR: Nothing totally automatic but two possible options.

Just to confirm - based on

And I'd like each continent to be an area where I could attach shapes to and drag/reshape them, while always being clipped by the continent's shape borders/limits.

I think you are asking how to limit the boundaries for dragging a shape to an 'arbitrary' region. I say arbitrary because it is a non-geometric region (not a square, circle, pentagon etc).

It would be fabulous to have a baked-in function to achieve this but sadly I am not aware that it is possible. Here's why:

Dragbounds limits: In terms of what you get 'out of the box', how Konva handles constraining drag position is via the node.dragBoundFunc() . Here is the example from the Konva docs which is straightforward.

// get drag bound function var dragBoundFunc = node.dragBoundFunc();

// create vertical drag and drop
node.dragBoundFunc(function(pos){
  // important pos - is absolute position of the node
  // you should return absolute position too
  return {
    x: this.absolutePosition().x,
    y: pos.y
  };
});

The gist of this is that we are able to use the code in the dragBoundFunc function to decide if we like the position the shape is being dragged to, or not. If not we can override that 'next' position with our own.

Ok - so that is how dragging is constrained via dragBoundFunc. You can also use the node.on('dragmove') to achieve the same effect - the code would be very similar.

Hit testing

To decide in the dragBoundFunc whether to accept the proposed position of the shape being dragged, we need to carry out 'hit testing'.

[Aside: An important consideration is that, to make a pleasing UI, we should be hit testing at the boundary of the shape that is being dragged - not where the mouse pointer or finger are positioned. Example - think of a circle being dragged with the mouse pointer at its center - we want to show the user the 'hit' UI when the perimeter of the circle goes 'out of bounds' from the perspective of the dragBoundFunc, not when the center hits that point. What this means in effect is that our logic should check the perimeter of the shape for collision with the boundary - that might be simple or more difficult, depending on the shape.]

So we know we want to hit test our dragging shape against an arbitrary, enclosing boundary (the country border).

Option #1: Konva built-in method.

Konva can help with its stage.getIntersection(pt) method. What this does is to look at a given pixel, from topmost down, of the shapes that might overlap the given x, y point. It stops at the first hit.

What point do we give it to check? So here's the rub - you would have to predefine points on your map that were on the borders. How many points? Enough so that your draggable shapes can't stray very far over the mesh of border points without the hit-test firing. Do it correctly and this would be a very efficient method of hit testing. And it's not as if the borders will be changing regularly.

Option #2: Alpha value checking.

The gist of this method is to have the color fill of each country have a specific alpha value in its RGBA setting. You can then check the colors at specific points on the perimeter of your dragging shape. Lets say we set the alpha for France to 250, the Channel is 249, Spain 248, Italy 247, etc. If you are dragging your shape around 'inside' France, you expect an alpha value of 250. If you see anything else under any of those perimeter points then some part of your shape has crossed the border. [In practice, the HTML canvas will add some antialiasing along the border line so you will see some values outside those that you set but these have a low impact affect and can be ignored.]

One point is that you can't test the color on the main canvas if the shape being dragged is visible - because you will be getting the fill, stroke, or antialised pixel color of the shape!

To solve this you need a second stage - this can be memory only, so not visible on the page - where you load either a copy of the main stage with the dragging shape invisible, or you load the image of the map only. Let's call this the hit-stage. Assuming you keep the position of the hit-stage in line with the main-stage, then everything will work. Based on the location of the dragging shape and its perimeter points, you check the pixel colors on the hit-canvas. If the values match the country you are expecting then no hit, but if you see a different alpha value then you hit or passed the border. Actually you don't even need to know the color for the starting country - just not the color under the mouse point when the drag commences and look out for a different alpha value under the perimeter points.

There's a working demo of the 2-stage approach at codePen here and I will include the code below. This does NOT demo the alpha-value-per-country but you should be able to extrapolate that from here on. I'll include the code in this answer for posterity.

This is the JavaScript from the codepen demo. The html is trivial.

const 
  scale = 1,
  stage = new Konva.Stage({
    container: "container",
    width: 500,
    height: 400,
    draggable: false,
  scale: {
    x: scale,
    y: scale
  }
  }),
      
  layer = new Konva.Layer({
    draggable: false
  }),
      
  imageShape = new Konva.Image({
    x: 0,
    y: 0,
    width: 500,
    height: 500,
    draggable: false
  }),
      
  circle = new Konva.Circle({
    radius: 30,
    fill: "magenta",
    draggable: true,
    x: 300,
    y: 100,
      scale: {
    x: scale,
    y: scale
  }
  });

layer.add(imageShape, circle);
stage.add(layer);

const hitStage = new Konva.Stage({
    container: "container2",
    width: 500,
    height: 400,
    draggable: false,
  }),
      hitLayer = new Konva.Layer(),
      hitImage = new Konva.Image();
      
hitLayer.add(hitImage);
hitStage.add(hitLayer);


// Make an HTML image variable to act as the image loader, load the image
const img = new Image();
img.crossOrigin = "Anonymous";
img.onload = function () {
  imageShape.image(img); // when loaded give the image to the Konva image shape
  
  // and now the stage is built, grab a copy for the hit canvas 
  const hitImageObj = new Image();
  hitImageObj.onload = function () {
    hitImage.image(hitImageObj);

    //  now snapshot is done we can make the circle visible.
    circle.visible(true);
  }

  // while we take this snapshot we need to hide the circle
  circle.visible(false); 

  // and set the hit canvas image source to load the image there
  hitImageObj.src = stage.toDataURL({imageSmoothingEnabled: false});
  
};
img.src = "https://assets.codepen.io/255591/shokunin_United_Kingdom_map.svg";

// Get the convas context - used later to get the pixel data
const ctx = hitLayer.getCanvas().getContext();

// Will run on each drag move event
circle.on("dragmove", function () {

  // get 20 points on the perimeter to check.
  let hit = false;
  for (let angle = 0; angle < 360; angle = angle + 18) {
    const angleRadians = (angle * Math.PI) / 180;
    let point = {
      x: parseInt(
        circle.position().x + Math.cos(angleRadians) * circle.radius(),
        10
      ),
      y: parseInt(
        circle.position().y + Math.sin(angleRadians) * circle.radius(),
        10
      )
    };

    // get the image data for one pixel at the computed point 
    const pixel = ctx.getImageData(point.x, point.y, 1, 1);
    const data = pixel.data;

    // for fun, we show the rgba value at the pixel
    const rgba = `rgba(${data[0]}, ${data[1]}, ${data[2]}, ${data[3] / 255})`;
    console.log("color at (" + point.x + ", " + point.y + "):", rgba);

    //
    // Here comes the good part. The Alpha value ranges from 0 - 1. In this case we expect it to be exactly 1. If it is less
    // than 1 then the pixel at this point is 'off' the solid color area so we know we have a hit on the border. If the value 
    // IS 1 then we are still safe inside the country. Note: you can reverse the image and have solid color outside and 
    // transparency inside the country - in this case test for alpha = 0 (good) or alpha > 0 (bad).
    hit = false;
    if ((data[3] / 255) < 1) {  // pixel data values are hex so divide by 255 to get decimal equiv.
      hit = true;
      break; // jumpt out of the loop now because we know we got a hit.
    }
  }
  
  // After checking the points what did the hit indicator show ?
  if (hit) {
    circle.fill("red");
  } else {
    circle.fill("magenta");
  }
  
});

PS. As a bonus, knowing the alpha values of the countries gives you an instant way to know which country the user clicks on.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM