Search code examples
javascripthtmlcssmouseevent

How to check a mouse pointer event is within a mask?


enter image description here

I need to highlight a puzzle piece on mouse hover. Each puzzle is a div, masked by an SVG. I figured out how to determine a mask bounding rectangle and check within it, but I can't figure out how to check events within complex SVG shape (Codepen — https://codepen.io/alexchekanov/pen/Yzjymgq):

<style>
  .tt-puzzle-container {
    position: relative;
    display: -webkit-box;
    display: -webkit-flex;
    display: -ms-flexbox;
    display: flex;
    width: 500px;
    height: 500px;
    -webkit-box-orient: vertical;
    -webkit-box-direction: normal;
    -webkit-flex-direction: column;
    -ms-flex-direction: column;
    flex-direction: column;
    -webkit-box-pack: end;
    -webkit-justify-content: flex-end;
    -ms-flex-pack: end;
    justify-content: flex-end;
    pointer-events: auto; /* added */
  }

  .tt-puzzle-piece {
    position: absolute;
    width: 100%;
    height: 50%;
    padding-right: 0px;
    padding-bottom: 0px;
    background-color: #ff0606;
    -webkit-transform-origin: 50% 0%;
    -ms-transform-origin: 50% 0%;
    transform-origin: 50% 0%;
    -webkit-transition: background-color 200ms ease;
    transition: background-color 200ms ease;
  }

  .tt-puzzle-piece:hover {
    background-color: #50ff06;
  }

  .tt-puzzle-piece._2 {
    z-index: 10;
    -webkit-transform: rotate(72deg);
    -ms-transform: rotate(72deg);
    transform: rotate(72deg);
    mask: var(--mask-url);
  }

  .tt-puzzle-piece._3 {
    z-index: 20;
    -webkit-transform: rotate(144deg);
    -ms-transform: rotate(144deg);
    transform: rotate(144deg);
    mask: var(--mask-url);
  }

  .tt-puzzle-piece._4 {
    z-index: 30;
    -webkit-transform: rotate(216deg);
    -ms-transform: rotate(216deg);
    transform: rotate(216deg);
    mask: var(--mask-url);
  }

  .tt-puzzle-piece._5 {
    z-index: 40;
    -webkit-transform: rotate(288deg);
    -ms-transform: rotate(288deg);
    transform: rotate(288deg);
    mask: var(--mask-url);
  }
</style>
<body>
  <div class="tt-puzzle-container">
    <div class="w-embed">
      <style>
        :root {
          --mask-url: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 238.5 209.5"><path d="M0.3,170.6c-0.5,0.7-0.3,1.6,0.4,2.1c14.8,10.1,30.8,18.3,47.5,24.3c0,0,0.1,0,0.1,0.1c1.1,0.4,2.1,0.8,3.2,1.1c0.3,0.1,0.6,0.2,0.9,0.3c0.9,0.3,1.8,0.6,2.7,0.9c0.6,0.2,1.1,0.4,1.7,0.5c0.7,0.2,1.3,0.4,2,0.6c1.3,0.4,2.6,0.8,3.8,1.1c1.4,0.4,2.8,0.8,4.2,1.1c0.5,0.1,1,0.2,1.5,0.4c0.5,0.1,1.1,0.3,1.6,0.4c0.5,0.1,1.1,0.3,1.6,0.4c1.6,0.4,3.2,0.7,4.8,1.1c0.5,0.1,1.1,0.2,1.6,0.3c1.1,0.2,2.2,0.4,3.2,0.6c0.5,0.1,1.1,0.2,1.6,0.3c1.1,0.2,2.2,0.4,3.3,0.6c0.6,0.1,1.1,0.2,1.6,0.3c1.6,0.2,3.2,0.5,4.8,0.7c0.6,0.1,1.2,0.2,1.8,0.2c0.4,0.1,0.8,0.1,1.3,0.1c0.4,0.1,0.9,0.1,1.3,0.1c0.7,0.1,1.3,0.1,1.9,0.2c0.2,0,0.4,0,0.6,0.1c30,2.8,60-0.9,87.9-10.5c12.1-4.1,23.7-9.4,34.9-15.7c3.5-2,6.9-4,10.3-6.2c1.8-1.1,3.5-2.3,5.3-3.5c0.7-0.5,0.9-1.4,0.4-2.1l-25.1-34.6c-0.8-1.1-2.5-1.1-3.2,0.1c-1.2,2-2.8,3.8-4.8,5.2c-0.8,0.5-1.6,1-2.4,1.5c-7.6,3.9-16.9,1.8-22.1-4.9c-3-3.8-4.2-8.6-3.6-13.4c0.7-4.8,3.2-9,7.1-11.9c2-1.4,4.1-2.4,6.4-2.9c1.3-0.3,1.9-1.9,1.1-3l-29.4-40.4c-0.5-0.6-1.3-0.8-2-0.4c0,0,0,0,0,0c-0.5,0.3-0.9,0.6-1.4,0.9c-0.4,0.2-0.8,0.4-1.1,0.7c-0.8,0.5-1.6,0.9-2.4,1.3c-0.3,0.1-0.5,0.3-0.8,0.4c-9.7,5-20.4,7.8-31.2,8.4c-0.3,0-0.6,0-0.9,0.1c-1,0-2,0.1-2.9,0.1c-0.4,0-0.9,0-1.3,0c-13.7-0.2-27.4-3.9-39.7-11.5c0,0-0.1,0-0.1,0c-0.1-0.1-0.3-0.2-0.4-0.2c-0.8,0.2-1.2,0.2-2,0.4l-34.5,47.5c-1.5,2.1-4.2,2.8-6.5,2c-0.5-0.2-1-0.4-1.4-0.8c-1.6-1.2-2.5-3.4-2.3-5.9c0-0.2,0-0.4-0.1-0.7c-3.4-9.2-14.2-9.1-18-2.1c-1.9,3.4-1.4,7.7,1.1,10.7c2.5,2.9,6.2,4.1,9.8,3.2c1.9-0.5,3.9-0.2,5.2,0.8c0.5,0.3,0.8,0.7,1.2,1.1c1.5,1.9,1.6,4.7,0.1,6.8L0.3,170.6z"/></svg>');
        }
        .tt-puzzle-piece {
          -webkit-mask: var(--mask-url) center/contain no-repeat;
          mask: var(--mask-url) center/contain no-repeat;
        }
      </style>
    </div>
    <div class="tt-puzzle-piece"></div>
    <div class="tt-puzzle-piece _2"></div>
    <div class="tt-puzzle-piece _3"></div>
    <div class="tt-puzzle-piece _4"></div>
    <div class="tt-puzzle-piece _5"></div>
  </div>
</body>

<script>
  const puzzleContainer = document.querySelector(".tt-puzzle-container");
  const puzzlePieces = document.querySelectorAll(".tt-puzzle-piece");

  function isEventWithinPuzzlePiece(event) {
    // Get the bounding rect of the puzzle container
    const puzzleContainerRect = puzzleContainer.getBoundingClientRect();

    // Check if the event occurred within the puzzle container rect
    return (
      event.clientX >= puzzleContainerRect.left &&
      event.clientX <= puzzleContainerRect.right &&
      event.clientY >= puzzleContainerRect.top &&
      event.clientY <= puzzleContainerRect.bottom
    );
  }

  function isEventWithinMask(event, puzzlePiece) {
    // Get the bounding rect of the mask
    const maskRect = puzzlePiece.getBoundingClientRect();

    // Check if the event occurred within the mask rect
    return (
      event.clientX >= maskRect.left &&
      event.clientX <= maskRect.right &&
      event.clientY >= maskRect.top &&
      event.clientY <= maskRect.bottom
    );
  }

  puzzleContainer.addEventListener("click", (event) => {
    // Check if the event occurred within a puzzle piece
    if (!isEventWithinPuzzlePiece(event)) {
      return;
    }

    // Get the element under the mouse
    const elementUnderMouse = document.elementFromPoint(
      event.clientX,
      event.clientY
    );
    if (!elementUnderMouse) {
      return;
    }

    // Check if the element under the mouse is a puzzle piece
    const puzzlePiece = elementUnderMouse.closest(".tt-puzzle-piece");
    if (!puzzlePiece) {
      return;
    }

    // Check if the event occurred within the mask of the puzzle piece
    if (isEventWithinMask(event, puzzlePiece)) {
      // Prevent the event from being handled
      event.stopPropagation();
    }
  });
</script>

Solution

  • (Text and code have been updated after comment by @herrstrietzel)

    Because individual <div> with a rotated SVG as background are rectangluar elements.

    • Despite masking the outsides of the SVG, the rectangle will still overlap sibbling elements. Using a mask-image with the puzzle pieces will remove all underlaying overlaps of their sibblings, leaving you with just a few lines as final result.

    • Without mask-image, only small parts of the individual pieces will react to :hover, despite changing z-index.

    I tested the above to be true in Firefox.

    Solution

    • Create an in-document SVG hidden from view with style="display: none" defining the puzzle piece as a <symbol> with and id and <use> it in your <div>.

    • Re<use> the puzzle piece four more times inside the <div> and transform: rotate them to their proper location.

    It took me quite some time to create the final SVG and the demo, so I did not incorporate your Javascript in the snippet. However, it should give you not much trouble attaching an eventlistener to the symbol and/or the individual path in the SVG to perform the required actions on hover/click.

    As you can see in the snippet, the final hover action only takes two CSS rules to achieve the desired result:

    .tt-puzzle-piece       { fill: red; transition: fill 0.2s ease }
    .tt-puzzle-piece:hover { fill: lime }
    

    BTW I used SVGedit v7.0 to create the full puzzle...

    UPDATE I changed the code based on herrstrietzel comment below and moved the code around as suggested.

    My previous solution did not work in Chrome/Edge, because the in-doc SVG gets put in shadow DOM and Chromium browser do not bubble hover into that DOM apparently, where Firefox does. People are not sure which User Agent developer is right as the W3C spec are not conclusive about which behaviour should be supported.

    The final code now works in Chrome/Edge and Firefox. By lack of Apple devices I did not test it in Safari.

    Check Stackoverflow Question 64633294 and further...

    snippet (with updated code).

    /* * { outline: 1px dashed } /* for debugging */
    
    
    * { box-sizing: border-box }
    
    body {
        margin: 0;
        width: 100%; min-height: 100vh;
        display: grid; place-items: center;
    }
    
    .tt-puzzle-container       { width: 50vw; height: 50vw }
    .tt-puzzle-container > svg { width: 100%; height: 100% }
    
    .tt-puzzle-piece           { fill: red; transition: fill 0.2s ease }
    .tt-puzzle-piece:hover     { fill: lime }
    <div class="tt-puzzle-container">
      <svg viewbox="0 0 418 418">
        <!-- would also work with external svgs: 
      <use class="tt-puzzle-piece" href="puzzlePiece.svg#puzzle-piece" />
      -->
        <use class="tt-puzzle-piece" href="#puzzle-piece" />
        <use class="tt-puzzle-piece" href="#puzzle-piece" transform="rotate(-72, 209, 209)" />
        <use class="tt-puzzle-piece" href="#puzzle-piece" transform="rotate(-144, 209, 209)" />
        <use class="tt-puzzle-piece" href="#puzzle-piece" transform="rotate(-216, 209, 209)" />
        <use class="tt-puzzle-piece" href="#puzzle-piece" transform="rotate(-288, 209, 209)" />
      </svg>
    </div>
    
    <!-- hidden or external svg: e.g "puzzlePiece.svg" -->
    <svg xmlns="http://www.w3.org/2000/svg" style="display:none">
      <symbol id="puzzle-piece" viewbox="0 0 418 418">
        <path d="m90.77,377.92c-0.5,0.7 -0.3,1.6 0.4,2.1c14.8,10.1 30.8,18.3 47.5,24.3c0,0 0.1,0 0.1,0.1c1.1,0.4 2.1,0.8 3.2,1.1c0.3,0.1 0.6,0.2 0.9,0.3c0.9,0.3 1.8,0.6 2.7,0.9c0.6,0.2 1.1,0.4 1.7,0.5c0.7,0.2 1.3,0.4 2,0.6c1.3,0.4 2.6,0.8 3.8,1.1c1.4,0.4 2.8,0.8 4.2,1.1c0.5,0.1 1,0.2 1.5,0.4c0.5,0.1 1.1,0.3 1.6,0.4c0.5,0.1 1.1,0.3 1.6,0.4c1.6,0.4 3.2,0.7 4.8,1.1c0.5,0.1 1.1,0.2 1.6,0.3c1.1,0.2 2.2,0.4 3.2,0.6c0.5,0.1 1.1,0.2 1.6,0.3c1.1,0.2 2.2,0.4 3.3,0.6c0.6,0.1 1.1,0.2 1.6,0.3c1.6,0.2 3.2,0.5 4.8,0.7c0.6,0.1 1.2,0.2 1.8,0.2c0.4,0.1 0.8,0.1 1.3,0.1c0.4,0.1 0.9,0.1 1.3,0.1c0.7,0.1 1.3,0.1 1.9,0.2c0.2,0 0.4,0 0.6,0.1c30,2.8 60,-0.9 87.9,-10.5c12.1,-4.1 23.7,-9.4 34.9,-15.7c3.5,-2 6.9,-4 10.3,-6.2c1.8,-1.1 3.5,-2.3 5.3,-3.5c0.7,-0.5 0.9,-1.4 0.4,-2.1l-25.1,-34.6c-0.8,-1.1 -2.5,-1.1 -3.2,0.1c-1.2,2 -2.8,3.8 -4.8,5.2c-0.8,0.5 -1.6,1 -2.4,1.5c-7.6,3.9 -16.9,1.8 -22.1,-4.9c-3,-3.8 -4.2,-8.6 -3.6,-13.4c0.7,-4.8 3.2,-9 7.1,-11.9c2,-1.4 4.1,-2.4 6.4,-2.9c1.3,-0.3 1.9,-1.9 1.1,-3l-29.4,-40.4c-0.5,-0.6 -1.3,-0.8 -2,-0.4c0,0 0,0 0,0c-0.5,0.3 -0.9,0.6 -1.4,0.9c-0.4,0.2 -0.8,0.4 -1.1,0.7c-0.8,0.5 -1.6,0.9 -2.4,1.3c-0.3,0.1 -0.5,0.3 -0.8,0.4c-9.7,5 -20.4,7.8 -31.2,8.4c-0.3,0 -0.6,0 -0.9,0.1c-1,0 -2,0.1 -2.9,0.1c-0.4,0 -0.9,0 -1.3,0c-13.7,-0.2 -27.4,-3.9 -39.7,-11.5c0,0 -0.1,0 -0.1,0c-0.1,-0.1 -0.3,-0.2 -0.4,-0.2c-0.8,0.2 -1.2,0.2 -2,0.4l-34.5,47.5c-1.5,2.1 -4.2,2.8 -6.5,2c-0.5,-0.2 -1,-0.4 -1.4,-0.8c-1.6,-1.2 -2.5,-3.4 -2.3,-5.9c0,-0.2 0,-0.4 -0.1,-0.7c-3.4,-9.2 -14.2,-9.1 -18,-2.1c-1.9,3.4 -1.4,7.7 1.1,10.7c2.5,2.9 6.2,4.1 9.8,3.2c1.9,-0.5 3.9,-0.2 5.2,0.8c0.5,0.3 0.8,0.7 1.2,1.1c1.5,1.9 1.6,4.7 0.1,6.8l-30.2,41.6z" />
      </symbol>
    </svg>