Home > OS >  Merge shapes that share the same border in CSS or JS
Merge shapes that share the same border in CSS or JS

Time:08-02

I am looking for a simple way to combine shapes via CSS or JS to achieve something like below:

What I am trying to achieve

At first I was trying to just overlay the basic shapes using before and after. But by doing so I can not create a border around the whole merged shape. So next I tried a solution using html canvas like:

var canvas=document.getElementById("canvas");
var ctx=canvas.getContext("2d");

ctx.lineWidth = 10;
ctx.strokeRect(30,20,150,250);
ctx.strokeRect(120,160,120,50);

ctx.globalCompositeOperation='destination-out';

//Filling the rects only keeps the border
ctx.fillStyle = "#FF0000";
ctx.fillRect(30,20,150,250);
ctx.fillRect(120,160,120,50); 
body{ background-color: green; }
<canvas id="canvas" width=300 height=300></canvas>

But by doing so I wasn't able to keep the custom white fill of the combined shape. So I am now asking for a way to either optimize my current approach to be able to set a custom background for my shape or a completely new approach.

As I need to get the onm ouseEnter event I didn't found using a svg-file suitable for my situation.

If there is any framework that would make this process easier, I am also happy to adapt.

CodePudding user response:

I'm not sure this is the most suitable solution for your situation but note that your canvas code can be modified to fill the merged area and detect mouse events just fine.

You don't have to call strokeRect() or fillRect() that do only create a temporary rectangle only for their call. You can create a way more complex path made of several rectangles and other commands. This complex path will then be filled a single entity, doing the "merging" for you. By stroking first and filling after, we can hide the junctions between each shapes.
And to detect if you're over the filled area, you can use ctx.isPointInPath():

var canvas=document.getElementById("canvas");
var ctx=canvas.getContext("2d");

// beware, we fill over the inner half of the stroke
// so only half of this value will be visible
ctx.lineWidth = 20;

let mouse = { x: -1, y: -1 };

function draw() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  ctx.beginPath();
  ctx.rect(30,20,150,250);
  ctx.rect(120,160,120,50);

  ctx.rect(350,130,50,130);
  // contrarily to rect(),
  // arc() doesn't call automatically moveTo()
  // so to avoid having a link between the previous point
  // and the beginning of the arc, we hav to call it ourselves
  // this would also be true with any other command, except roundRect()
  ctx.moveTo(375 70, 80);
  ctx.arc(375, 80, 70, 0, Math.PI*2);

  // check if the mouse is over either the stroke or the fill area
  const hovering = ctx.isPointInStroke(mouse.x, mouse.y) ||
                     ctx.isPointInPath(mouse.x, mouse.y)
  // change the color based on hover state
  ctx.fillStyle = hovering ? "#FF0000" : "#FFFF00";

  // It's all a single path
  ctx.stroke(); // stroking would show all the various sections
  ctx.fill(); // but since we fill over it, we only keep the common outline
}
draw();
// update the hovering state
addEventListener("mousemove", (evt) => {
  const rect = canvas.getBoundingClientRect();
  mouse.x = evt.clientX - rect.left;
  mouse.y = evt.clientY - rect.top;
  draw();
});
body{ background-color: green; }
<canvas id="canvas" width=600 height=300></canvas>

And if you need you could separate each shape in their own Path2D object:

var canvas=document.getElementById("canvas");
var ctx=canvas.getContext("2d");

ctx.lineWidth = 20;

let mouse = { x: -1, y: -1 };

const p1 = new Path2D();
p1.rect(30,20,150,250);
p1.rect(120,160,120,50);

const p2 = new Path2D();
p2.rect(350,130,50,130);
p2.moveTo(375 70, 80);
p2.arc(375, 80, 70, 0, Math.PI*2);

function draw() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  ctx.beginPath();

  const overP1 = ctx.isPointInStroke(p1, mouse.x, mouse.y) ||
                   ctx.isPointInPath(p1, mouse.x, mouse.y)
  ctx.fillStyle = overP1 ? "#FF0000" : "#FFFF00";
  ctx.stroke(p1);
  ctx.fill(p1);
  
  const overP2 = ctx.isPointInStroke(p2, mouse.x, mouse.y) ||
                   ctx.isPointInPath(p2, mouse.x, mouse.y)
  ctx.fillStyle = overP2 ? "#FF0000" : "#FFFF00";
  ctx.stroke(p2);
  ctx.fill(p2);
}
draw();

addEventListener("mousemove", (evt) => {
  const rect = canvas.getBoundingClientRect();
  mouse.x = evt.clientX - rect.left;
  mouse.y = evt.clientY - rect.top;
  draw();
});
body{ background-color: green; }
<canvas id="canvas" width=600 height=300></canvas>

CodePudding user response:

Let's reduce the problem. Build the shapes from atomic shapes such as rectangles and circles. Then you can extrapolate for many shapes. At first I thought about canvas using this function to detect mouse over:

const pointInRect = ({x1, y1, x2, y2}, {x, y}) => (
  (x > x1 && x < x2) && (y > y1 && y < y2)
)

But then again, since we reduced the problem we might as well use HTML. Since we are dealing with multiple shapes, it will be easier using some 2D map.

var shape = [
  " 1 1111 ".split(""),
  "11 1  1 ".split(""),
  " 111 111".split(""),
  "  1  1 1".split(""),
  "  1111 1 ".split(""),
]
const square_size = 40;
var container = document.querySelector(".container");

function draw_square(x, y) {
  var div = document.createElement("div");
  div.classList.add("square")
  div.style.left = x   "px"
  div.style.top = y   "px"
  container.appendChild(div)
  return div;
}

function draw_shape() {
  for (var i = 0; i < shape.length; i  ) {
    for (var j = 0; j < shape[i].length; j  ) {
      var pixel = shape[i][j];
      if (pixel == 1) {
        var div = draw_square(j * square_size, i * square_size);

        if (i > 0 && shape[i - 1][j] == 1) {
          div.style.borderTopColor = "transparent";
        }
        if (i < shape.length - 1 && shape[i   1][j] == 1) {
          div.style.borderBottomColor = "transparent";
        }
        if (j > 0 && shape[i][j - 1] == 1) {
          div.style.borderLeftColor = "transparent";
        }
        if (j < shape[i].length - 1 && shape[i][j   1] == 1) {
          div.style.borderRightColor = "transparent";
        }

      }
    }
  }
}

draw_shape(shape)
attach_mouse_events()

function attach_mouse_events() {
  var squares = document.querySelectorAll(".square");
  squares.forEach(function (square) {
    square.addEventListener('mouseenter', function(ev) {
      squares.forEach(function (square) {
        square.classList.add("active");
      })
    })
    square.addEventListener('mouseleave', function(ev) {
      squares.forEach(function (square) {
        square.classList.remove("active");
      })
    })    
  })

}
body {
  background: lightgray;
}

.square {
  width: 40px;
  height: 40px;
  background: green;
  border: 2px solid black;
  position: absolute;
}

.square.active {
   background: white;
}

.container {
  margin: 20px;
  background-size:cover;
  height: 300px;
  position: relative;
}
<div >
</div>

As for the circles - it's the same idea. I would recommend painting the circles first then the squares, for a perfect borders. Good luck.

  • Related