Search code examples
javascripthtmljquerycanvasfabricjs

How to make DOM element that is placed on top of fabricjs object stay there even when scrolling horizontally


I've got a canvas that has an array of objects, these objects are positioned and added to the canvas. Each object has a matching DOM element that triggers a tooltip. This element is placed exactly on top of the canvas object.

On desktop this works fine since the background image is always filling the screen and there is no scrollbar. But on mobile I have a horizontal scrollbar so users can scroll left and right on the image (else it will become to small).

The problem is, the DOM elements that are positioned on top of the fabric objects stay in their spot according to where the objects are without any scrolling, when I scroll horizontally, the DOM elements keep in their same spot.

I made a video on my phone that shows this: https://streamable.com/xn1t2i Dots with circle are the DOM elements outside the canvas that are placed on the canvas objects (blue dots without circles).

So I thought of the following solution: put the entire script inside a function, and call that function on an event, like: touchmove this however is very slow and shows a lot of flickering when moving around. So I tried touchend but this also is pretty slow and also triggers the function when clicking the tooltip.

Example video of touchmove: https://streamable.com/708d2s As you can see the dots do get repositioned, but way too slow and if the scroll drags on a bit too long, the dots get mis-aligned again.

I've also tried scroll but this didn't work at all.

This is my code at the moment:

javascript:

(function() {
     function reRender(){
            var myImg = document.querySelector("#background");
            if(window.outerWidth > 767) {
                menuheightcanvas = 172.5;
                var realWidth = window.innerWidth;
                var realHeight = myImg.naturalHeight;
            }else{
                menuheightcanvas = 99.8;
                var realWidth = myImg.naturalWidth - 900;
                var realHeight = myImg.naturalHeight;
            }
            var source = document.getElementById('background').src;
            var canvas = new fabric.Canvas('c', {
                allowTouchScrolling: true,
                selection: false
            });
            canvas.allowTouchScrolling = true;
            canvas.hoverCursor = 'pointer';
            canvas.setDimensions({
                    width: realWidth,
                    height: realHeight
            });
            var img = new Image();
            // use a load callback to add image to canvas.
            img.src = 'https://printzelf.nl/new/assets/images/custom/WOONKAMER.jpg';
            fabric.Object.NUM_FRACTION_DIGITS = 10;
            fabric.Image.fromURL(source, function(img) {
                img.scaleToWidth(canvas.width);
                canvas.setBackgroundImage(img);
                canvas.requestRenderAll();
        });

        var scaleToWidth = realWidth / myImg.width;

        // alert (scaleToWidth)
        const hotspots = [{
                        top: (140* scaleToWidth),
                        left: (720* scaleToWidth),
                        radius: 10,
                        fill: '#009fe3',
                        id: 'cirkel1',
                        hoverCursor: 'pointer',
                        selectable: false,
                        imgtop: 71,
                        imgleft: 236,
                        imgheight: 335,
                        imgwidth: 514,
                        placement: 'right',
                        tooltipid: 'cirkel1',
                        imgUrl: 'https://printzelf.nl/new/cms/images/canvas/woonkamer/gordijnen.jpg'
                },
                {
                        top: (160* scaleToWidth),
                        left: (640* scaleToWidth),
                        radius: 10,
                        fill: '#009fe3',
                        id: 'cirkel2',
                        hoverCursor: 'pointer',
                        selectable: false,
                        imgtop: 82,
                        imgleft: 351,
                        imgheight: 313,
                        imgwidth: 337,
                        placement: 'right',
                        tooltipid: 'cirkel2',
                        imgUrl: 'https://printzelf.nl/new/cms/images/canvas/woonkamer/voile.jpg'
                },
                {
                        top: (350* scaleToWidth),
                        left: (120* scaleToWidth),
                        radius: 10,
                        fill: '#009fe3',
                        id: 'cirkel3',
                        hoverCursor: 'pointer',
                        selectable: false,
                        imgtop: 293,
                        imgleft: 21,
                        placement: 'right',
                        imgheight: 81,
                        imgwidth: 107,
                        imgUrl: 'https://printzelf.nl/new/cms/images/canvas/woonkamer/fotoblok.jpg'
                },
                {
                        top: (275* scaleToWidth),
                        left: (165* scaleToWidth),
                        radius: 10,
                        fill: '#009fe3',
                        id: 'cirkel4',
                        hoverCursor: 'pointer',
                        selectable: false,
                        imgtop: 283,
                        imgleft: 127,
                        placement: 'right',
                        imgheight: 60,
                        imgwidth: 57,
                        imgUrl: 'https://printzelf.nl/new/cms/images/canvas/woonkamer/fotopaneel.jpg'
                },
                {
                        top: (430* scaleToWidth),
                        left: (600* scaleToWidth),
                        radius: 10,
                        fill: '#009fe3',
                        id: 'cirkel5',
                        hoverCursor: 'pointer',
                        selectable: false,
                        imgtop: 365,
                        imgleft: 227,
                        placement: 'right',
                        imgheight: 185,
                        imgwidth: 396,
                        imgUrl: 'https://printzelf.nl/new/cms/images/canvas/woonkamer/zitzak.jpg'
                }
        ];

        const loadedImages = [];

        for (let [idx, props] of hotspots.entries()) {
                let c = new fabric.Circle(props);
                c.class = 'hotspot';
                c.name = 'hotspot-' + idx;
                canvas.add(c);
        }

        fabric.Canvas.prototype.getAbsoluteCoords = function(object) {
                return {
                        left: object.left + this._offset.left,
                        top: object.top + menuheightcanvas
                };
        }

        var btnWidth = 40,
                btnHeight = 40;

        function positionBtn(obj, index) {
                var absCoords = canvas.getAbsoluteCoords(obj);
                var element = document.getElementById('cirkel' + index);
                element.style.left = (absCoords.left - btnWidth / 10) + 'px';
                element.style.top = (absCoords.top - btnHeight / 10) + 'px';
        }

        canvas.getObjects().forEach(function(ho, index) {
                positionBtn(ho, index + 1);
        });

        $(".canvastip").each(function(i) {
                tippy(this, {
                        theme: 'blue',
                        allowHTML: true,
                        placement: 'right',
                        animation: 'scale-subtle',
                        interactive: true,
                        // popperOptions: {
                        //  strategy: 'fixed',
                        //  modifiers: [
                        //      {
                        //          name: 'flip',
                        //          options: {
                        //              fallbackPlacements: ['bottom', 'bottom'],
                        //          },
                        //      },
                        //      {
                        //          name: 'preventOverflow',
                        //          options: {
                        //              altAxis: true,
                        //              tether: false,
                        //          },
                        //      },
                        //  ],
                        // },
                        onShow(instance) {
                                canvas.getObjects().forEach(function(ho, index) {
                                        if (ho.class && ho.class === 'hotspot') {
                                                if (instance.id == index + 1) {
                                                        // check if image was previously loaded
                                                        if (loadedImages.indexOf(ho.name) < 0) {
                                                                // image is not in the array
                                                                // so it needs to be loaded
                                                                // prepare the image properties
                                                                let imgProps = {
                                                                        width: ho.imgwidth,
                                                                        height: ho.imgheight,
                                                                        left: ho.imgleft* scaleToWidth,
                                                                        top: ho.imgtop* scaleToWidth,
                                                                        scaleX: 1* scaleToWidth,
                                                                        scaleY: 1* scaleToWidth,
                                                                        selectable: false,
                                                                        id: 'img-' + ho.name,
                                                                        hoverCursor: "default",
                                                                };
                                                                instance.setProps({placement: ho.placement})
                                                                var printzelfImg = new Image();
                                                                printzelfImg.onload = function(img) {
                                                                        var printzelf = new fabric.Image(printzelfImg, imgProps);
                                                                        printzelf.trippyHotspotImage = true;
                                                                        canvas.add(printzelf);
                                                                };
                                                                printzelfImg.src = ho.imgUrl;
                                                                // update the `loadedImages` array
                                                                loadedImages.push(ho.name);
                                                        } else {
                                                                for (const o of canvas.getObjects()) {
                                                                        if (o.id && o.id === 'img-' + ho.name) {
                                                                                o.visible = true;
                                                                                break;
                                                                        }
                                                                }
                                                                canvas.renderAll();
                                                        }
                                                }
                                        }
                                });
                        },
                        onHide(instance) {
                                for (const o of canvas.getObjects()) {
                                        if (o.trippyHotspotImage) {
                                                o.visible = false;
                                        }
                                }
                                canvas.renderAll();
                        },
                        content: function(reference) {
                                return reference.querySelector('.tooltipcontentcanvas' + (i + 1));
                        }
                });
        });
     }
     window.addEventListener('scroll', reRender, false);
     reRender();
})();

HTML:

<img id="background" src="https://printzelf.nl/new/assets/images/custom/WOONKAMER.jpg" alt="" style="display:none;">
<div class="canvas-container" style="width: 100%; position: relative;">
  <canvas id="c" width="100%" height="500" class="lower-canvas" style="position: absolute; width: 100%; height: 500px; left: 0px; top: 0px;"></canvas>
</div>
<span id="cirkel1" class="canvastip" style="border-radius:100%;width: 25px;height:25px;position:absolute;cursor:pointer;">
  <div class="tooltipcontentcanvas1 tooltipcontentcanvas darktext" style="position:relative;">
    <div class="tooltipwrap">
      <a href="product/gordijnen" title="Weet je wat ik graag zou willen zijn?.." alt="Weet je wat ik graag zou willen zijn?.." class="tooltipprodlink">
        <span class="tooltipprodlink">v.a. <b>€19,36</b> p/m<sup>2</sup></span>
        <img class="tooltipimgprod" src="cms/images/canvas/woonkamer/tooltip/gordijnen.jpg" alt="Gordijnen">
      </a>
      <div class="tooltipinfo">
        <span class="toptitle">Gordijnen</span>
        <h2>Weet je wat ik graag zou willen zijn?..</h2>
        <span class="sub">
          <ul>
            <li><img class="vinkje" src="https://printzelf.nl/new/assets/images/custom/vinkje.gif">10 materialen</li>
            <li><img class="vinkje" src="https://printzelf.nl/new/assets/images/custom/vinkje.gif">+ handige accessoires</li>
            <li><img class="vinkje" src="https://printzelf.nl/new/assets/images/custom/vinkje.gif">Contourfrezen mogelijk</li>
          </ul>
        </span>
        <a href="product/gordijnen" title="Weet je wat ik graag zou willen zijn?.." alt="Weet je wat ik graag zou willen zijn?.."><span class="btnstyle purplebtn">Stel gordijnen samen</span></a>
      </div>
    </div>
  </div>
</span>
<span id="cirkel2" class="canvastip" style="border-radius:100%;width: 25px;height:25px;position:absolute;cursor:pointer;">
  <div class="tooltipcontentcanvas2 tooltipcontentcanvas darktext" style="position:relative;">
    <div class="tooltipwrap">
      <a href="product/vitrage" title="Jouw vitrage wordt een rage!" alt="Jouw vitrage wordt een rage!" class="tooltipprodlink">
        <span class="tooltipprodlink">v.a. <b>€19,36</b> p/m<sup>2</sup></span>
        <img class="tooltipimgprod" src="cms/images/canvas/woonkamer/tooltip/vitragegordijnen.jpg" alt="Vitragegordijnen">
      </a>
      <div class="tooltipinfo">
        <span class="toptitle">Vitragegordijnen</span>
        <h2>Jouw vitrage wordt een rage!</h2>
        <span class="sub">
          <ul>
            <li><img class="vinkje" src="https://printzelf.nl/new/assets/images/custom/vinkje.gif">10 materialen</li>
            <li><img class="vinkje" src="https://printzelf.nl/new/assets/images/custom/vinkje.gif">+ handige accessoires</li>
            <li><img class="vinkje" src="https://printzelf.nl/new/assets/images/custom/vinkje.gif">Contourfrezen mogelijk</li>
          </ul>
        </span>
        <a href="product/vitrage" title="Jouw vitrage wordt een rage!" alt="Jouw vitrage wordt een rage!"><span class="btnstyle purplebtn">Stel vitragegordijnen samen</span></a>
      </div>
    </div>
  </div>
</span>
<span id="cirkel3" class="canvastip" style="border-radius:100%;width: 25px;height:25px;position:absolute;cursor:pointer;">
  <div class="tooltipcontentcanvas3 tooltipcontentcanvas darktext" style="position:relative;">
    <div class="tooltipwrap">
      <a href="product/fotoblok" title="Dit blok staat als een huis in je huis!" alt="Dit blok staat als een huis in je huis!" class="tooltipprodlink">
        <span class="tooltipprodlink">v.a. <b>€19,36</b> p/m<sup>2</sup></span>
        <img class="tooltipimgprod" src="cms/images/canvas/woonkamer/tooltip/fotoblok.jpg" alt="Fotoblok">
      </a>
      <div class="tooltipinfo">
        <span class="toptitle">Fotoblok</span>
        <h2>Dit blok staat als een huis in je huis!</h2>
        <span class="sub">
          <ul>
            <li><img class="vinkje" src="https://printzelf.nl/new/assets/images/custom/vinkje.gif">10 materialen</li>
            <li><img class="vinkje" src="https://printzelf.nl/new/assets/images/custom/vinkje.gif">+ handige accessoires</li>
            <li><img class="vinkje" src="https://printzelf.nl/new/assets/images/custom/vinkje.gif">Contourfrezen mogelijk</li>
          </ul>
        </span>
        <a href="product/fotoblok" title="Dit blok staat als een huis in je huis!" alt="Dit blok staat als een huis in je huis!"><span class="btnstyle purplebtn">Stel fotoblok samen</span></a>
      </div>
    </div>
  </div>
</span>
<span id="cirkel4" class="canvastip" style="border-radius:100%;width: 25px;height:25px;position:absolute;cursor:pointer;">
  <div class="tooltipcontentcanvas4 tooltipcontentcanvas darktext" style="position:relative;">
    <div class="tooltipwrap">
      <a href="product/foto-op-paneel" title="Zet jouw lievelingsfoto op een paneel" alt="Zet jouw lievelingsfoto op een paneel" class="tooltipprodlink">
        <span class="tooltipprodlink">v.a. <b>€19,36</b> p/m<sup>2</sup></span>
        <img class="tooltipimgprod" src="cms/images/canvas/woonkamer/tooltip/fotopaneel.jpg" alt="Fotopaneel">
      </a>
      <div class="tooltipinfo">
        <span class="toptitle">Fotopaneel</span>
        <h2>Zet je lievelingsfoto op een paneel</h2>
        <span class="sub">
          <ul>
            <li><img class="vinkje" src="https://printzelf.nl/new/assets/images/custom/vinkje.gif">10 materialen</li>
            <li><img class="vinkje" src="https://printzelf.nl/new/assets/images/custom/vinkje.gif">+ handige accessoires</li>
            <li><img class="vinkje" src="https://printzelf.nl/new/assets/images/custom/vinkje.gif">Contourfrezen mogelijk</li>
          </ul>
        </span>
        <a href="product/foto-op-paneel" title="Zet jouw lievelingsfoto op een paneel" alt="Zet jouw lievelingsfoto op een paneel"><span class="btnstyle purplebtn">Stel fotopaneel samen</span></a>
      </div>
    </div>
  </div>
</span>
<span id="cirkel5" class="canvastip" style="border-radius:100%;width: 25px;height:25px;position:absolute;cursor:pointer;">
  <div class="tooltipcontentcanvas5 tooltipcontentcanvas darktext" style="position:relative;">
    <div class="tooltipwrap">
      <a href="product/zitzak" title="Geniet rustig van jouw ontwerp" alt="Geniet rustig van jouw ontwerp" class="tooltipprodlink">
        <span class="tooltipprodlink">v.a. <b>€19,36</b> p/m<sup>2</sup></span>
        <img class="tooltipimgprod" src="cms/images/canvas/woonkamer/tooltip/zitzak.jpg" alt="Zitzak">
      </a>
      <div class="tooltipinfo">
        <span class="toptitle">Zitzakken</span>
        <h2>Geniet rustig van jouw ontwerp</h2>
        <span class="sub">
          <ul>
            <li><img class="vinkje" src="https://printzelf.nl/new/assets/images/custom/vinkje.gif">10 materialen</li>
            <li><img class="vinkje" src="https://printzelf.nl/new/assets/images/custom/vinkje.gif">+ handige accessoires</li>
            <li><img class="vinkje" src="https://printzelf.nl/new/assets/images/custom/vinkje.gif">Contourfrezen mogelijk</li>
          </ul>
        </span>
        <a href="product/zitzak" title="Geniet rustig van jouw ontwerp" alt="Geniet rustig van jouw ontwerp"><span class="btnstyle purplebtn">Stel zitzak samen</span></a>
      </div>
    </div>
  </div>
</span>

Codepen of entire page: https://codepen.io/twan2020/pen/VwPZmJx maybe try to view this on your phone, because for some reason when resizing the screen to mobile size on desktop breaks the canvas. On my mobile it works fine though.

How can I make sure the DOM element dots always stay on the canvas object dots? While keeping the speed it currently it has?


Solution

  • wrap a new relative positioned div around your .canvas-container and your #cirkel1 ... #cirkelN divs so that the relevant html code area is structured like this:

    <div class="canvas-container-container">
      <div class="canvas-container">...</div>
      <span id="cirkel1">...</span>
      <span id="cirkel2">...</span>
      ...
    </div>
    

    It is important that .canvas-container-container has position: relative; in order to position its' absolute positioned children #cirkel1 ... #cirkelN relative to the origin of .canvas-container-container. This change implies that the origin of .canvas-container-container corresponds to the (0,0) point in your canvas, which solves the root problem of your issue.

    Update your css code so that the css rules of .canvas-container become the rules of canvas-container-container (make sure you remove the corresponding .canvas-container css code):

    .canvas-container-container {
      position: relative;
      height: 500px;
    }
    @media only screen and (max-width:991px) {
      .canvas-container-container{
        overflow-x:auto;
        overflow-y:hidden;
      }
      .canvas-container-container,
      .canvas-container {
        height: 400px;
      }
    }
    

    With that the horizontal scrolling should be working already. However since your #cirkel1 ... #cirkelN are now positioned relatively to .canvas-container-container you won't need to add this._offset.left and menuheightcanvas in your getAbsoluteCoords anymore:

    fabric.Canvas.prototype.getAbsoluteCoords = function(object) {
      return {
        left: object.left, // +this._offset.left not needed here
        top: object.top, // +menuheightcanvas not needed here
      };
    }
    

    In fact you won't even need this getAbsoluteCoords at all since obj.left and obj.top are already relative to canvas origin and thus don't need the offset:

    function positionBtn(obj, index) {
      var element = document.getElementById('cirkel' + index);
      element.style.left = (obj.left - btnWidth / 10) + 'px';
      element.style.top = (obj.top - btnHeight / 10) + 'px';
    }