Search code examples
javascriptcanvashtml5-canvasfabricjs

loadFromJSON at different screen resolutions and aspect ratios


I need to be able to use loadFromJSON with canvas resolutions/aspect ratios other than the one that the JSON data was generated at, while maintaining the relationships of the graphic elements and centering everything within the canvas.

I have tried every solution I've seen or have come up with. At this point I don't know if it is my logic that is bad, my arithmetic or my coding.

Functional code is included here (stripped of my failed "attempts"). Only the first function matters – the rest is UI boilerplate and JSON data. Here's a JS Fiddle if that is easier: https://jsfiddle.net/sunny001/a8thqd0z/24/

Details: I save the JSON data with custom width and height properties so that I know the resolution/aspect ratio the data was created at. I then use those properties to determine how to scale things. The canvas is always set to the size of the window, which can vary. I've seen some solutions which use the canvas "zoom" property but I can't do that because the app allows the user to zoom in on the documents they are annotating.

Background This is for a desktop electron app where the user can annotate text documents, so accurate positioning matters. The user can create and present the annotations in a window mode or full screen.

'use strict';


let canvasA
let canvasB

// these are the canvas dimensions
let A = {
  width: 320,
  height: 190
}
let B = {
  width: 225,
  height: 150
}

function loadAnnotation(theCanvas, theData, id) {

  theCanvas.clear()

  var containerWidth = theCanvas.getWidth()
  var containerHeight = theCanvas.getHeight()
  var originalWidth = theData.width
  var originalHeight = theData.height
  var scaleFactor

  theCanvas.loadFromJSON(theData, function() {

    /**
     * the canvas seems to change size "on its own" based on the JSON data
     * width & height properties so this hack resets it 
     * 
     * setDimensions() seems buggy – screen redraw artifacts so using setWidth() & setheight()
     * 
     **/
    if (id == "A") {
      // theCanvas.setDimensions(A.width, A.height)
      theCanvas.setWidth(A.width)
      theCanvas.setHeight(A.height)
    } else {
      // theCanvas.setDimensions(B.width, B.height)
      theCanvas.setWidth(B.width)
      theCanvas.setHeight(B.height)
    }

    // just logging code
    if (id == "A") {
      logA(`data w/h:  ${originalWidth} x ${originalHeight}`)
      logA(`canvas w/h:  ${theCanvas.getWidth()} x ${theCanvas.getHeight()}`)
      logA(`canvas zoom:  ${theCanvas.getZoom()}`)
      logA(`scaleFactor: ${scaleFactor}`)
    } else {
      logB(`data w/h:  ${originalWidth} x ${originalHeight}`)
      logB(`canvas w/h:  ${theCanvas.getWidth()} x ${theCanvas.getHeight()}`)
      logB(`canvas zoom:  ${theCanvas.getZoom()}`)
      logB(`scaleFactor: ${scaleFactor}`)
    }

  }, function(o, object) {

    var widthRatio = containerWidth / originalWidth
    var heightRatio = containerHeight / originalHeight

    // if (widthRatio <= heightRatio) {
    if (widthRatio > heightRatio) {
      scaleFactor = widthRatio
    } else {
      scaleFactor = heightRatio
    }

    object.scaleX = object.scaleX * scaleFactor
    object.scaleY = object.scaleY * scaleFactor
    object.left = object.left * scaleFactor
    object.top = object.top * scaleFactor

    object.setCoords()
  })

  theCanvas.renderAll();
  theCanvas.calcOffset();
}




// Everything below here is UI code & JSON data

document.addEventListener("DOMContentLoaded", (event) => {
  var today = new Date();
  var time = today.getHours() + ":" + today.getMinutes() + ":" + today.getSeconds();
  console.log(time)

  var canvasAloadA = document.querySelector("#canA-loadA")
  canvasAloadA.addEventListener("click", function(event) {
    logA()
    loadAnnotation(canvasA, canvasA_data, "A")
  })

  var canvasAloadB = document.querySelector("#canA-loadB")
  canvasAloadB.addEventListener("click", function(event) {
    logA()
    loadAnnotation(canvasA, canvasB_data, "A")
  });

  var clearA = document.querySelector("#canA-clear")
  clearA.addEventListener("click", function(event) {
    canvasA.clear()
    logA()
  });


  var saveA = document.querySelector("#canA-save")
  saveA.addEventListener("click", function(event) {

    canvasA_data = canvasA.toObject(["width", "height"])
    // canvasA_data = JSON.stringify(canvasA.toObject(["width", "height"]));
    // console.log('Save. A', JSON.stringify(canvasA_data))
  });


  var canvasBloadA = document.querySelector("#canB-loadA")
  canvasBloadA.addEventListener("click", function(event) {
    logB()
    loadAnnotation(canvasB, canvasA_data, "B")
  })

  var canvasBloadB = document.querySelector("#canB-loadB")
  canvasBloadB.addEventListener("click", function(event) {
    logB()
    loadAnnotation(canvasB, canvasB_data, "B")
  });

  var clearB = document.querySelector("#canB-clear")
  clearB.addEventListener("click", function(event) {
    canvasB.clear()
    logB()
  });

  var saveB = document.querySelector("#canB-save")
  saveB.addEventListener("click", function(event) {
    canvasB_data = canvasB.toObject(["width", "height"])
    // canvasB_data = JSON.stringify(canvasB.toObject(["width", "height"]));
    console.log('Save. B', JSON.stringify(canvasB_data))
  })

  setUpFabric()
})


function setUpFabric() {

  canvasA = new fabric.Canvas('canvas-A', {
    backgroundColor: '#FFFFFF',
    width: 320,
    height: 190
  })

  loadAnnotation(canvasA, canvasA_data, "A")
  // canvasA_LoadData() // only used to generate initial JSON data
  canvasA.renderAll();

  canvasB = new fabric.Canvas('canvas-B', {
    backgroundColor: '#FFFFFF',
    width: 225,
    height: 150
  });

  loadAnnotation(canvasB, canvasB_data, "B")
  // canvasB_LoadData() // only used to generate initial JSON data

  canvasB.renderAll();
}

/**
 * fitInBox
 * Constrains a box (width x height) to fit in a containing box (maxWidth x maxHeight), preserving the aspect ratio
 * @param width      width of the box to be resized
 * @param height     height of the box to be resized
 * @param maxWidth   width of the containing box
 * @param maxHeight  height of the containing box
 * @param expandable (Bool) if output size is bigger than input size, output is left unchanged (false) or expanded (true)
 * @return           {width, height} of the resized box
 */
function fitInBox(width, height, maxWidth, maxHeight, expandable) {
  "use strict";

  var aspect = width / height,
    initWidth = width,
    initHeight = height;

  if (width > maxWidth || height < maxHeight) {
    width = maxWidth;
    height = Math.floor(width / aspect);
  }

  if (height > maxHeight || width < maxWidth) {
    height = maxHeight;
    width = Math.floor(height * aspect);
  }

  if (!!expandable === false && (width >= initWidth || height >= initHeight)) {
    width = initWidth;
    height = initHeight;
  }

  return {
    width: width,
    height: height
  };
}



function logA(txt) {
  if (txt == undefined) {
    document.getElementById('canA').value = ""
  } else {
    document.getElementById('canA').value += `\n${txt}`
  }
}



function logB(txt) {
  if (txt == undefined) {
    document.getElementById('canB').value = ""
  } else {
    document.getElementById('canB').value += `\n${txt}`
  }
}


// this is for initial JSON data setup only - not used in demo
function canvasA_LoadData() {

  var elA = document.getElementById('test-imageA');
  var imgA = new fabric.Image(elA, {
    left: 0,
    top: 0,
    selectable: true
  })
  canvasA.add(imgA);

  var containerWidth = canvasA.getWidth()
  var containerHeight = canvasA.getHeight()

  var result = fitInBox(imgA.width, imgA.height, containerWidth, containerHeight, true)

  imgA.scaleToWidth(result.width)

  var xOffset = (containerWidth - imgA.getScaledWidth()) / 2
  var yOffset = (containerHeight - imgA.getScaledHeight()) / 2

  imgA.set({
    left: xOffset,
    top: yOffset
  })
  imgA.setCoords()

  var rect = new fabric.Rect({
    left: 100,
    top: 0,
    fill: 'red',
    width: 20,
    height: 20
  });

  var circle = new fabric.Circle({
    radius: 20,
    stroke: 'green',
    strokeWidth: 12,
    fill: null,
    left: 200,
    top: 130
  });

  var triangle = new fabric.Triangle({
    width: 40,
    height: 40,
    fill: 'blue',
    left: 50,
    top: 140
  });

  var txt = new fabric.Text("Canvas A 320 x 190", {
    fontSize: 24,
    left: 50,
    top: 50,
    fill: 'white'
  })

  canvasA.add(rect, circle, triangle, txt);

  canvasA.calcOffset();
  canvasA.renderAll();
}

// this is for initial JSON data setup only - not used in demo
function canvasB_LoadData() {

  var elB = document.getElementById('test-imageB');

  var imgB = new fabric.Image(elB, {
    left: 0,
    top: 0,
    selectable: true
  })
  canvasB.add(imgB)

  var containerWidth = canvasB.getWidth()
  var containerHeight = canvasB.getHeight()

  var result = fitInBox(imgB.width, imgB.height, containerWidth, containerHeight, true)

  imgB.scaleToWidth(result.width)

  var xOffset = (containerWidth - imgB.getScaledWidth()) / 2
  var yOffset = (containerHeight - imgB.getScaledHeight()) / 2

  imgB.set({
    left: xOffset,
    top: yOffset
  })
  imgB.setCoords()

  var rect = new fabric.Rect({
    left: 0,
    top: 0,
    fill: 'orange',
    width: 60,
    height: 60
  })

  var circle = new fabric.Circle({
    radius: 40,
    stroke: 'red',
    strokeWidth: 12,
    fill: null,
    left: 120,
    top: 40
  })

  var triangle = new fabric.Triangle({
    width: 40,
    height: 40,
    fill: 'black',
    left: 50,
    top: 100
  });

  var txt = new fabric.Text("Canvas B 225 x 150", {
    fontSize: 20,
    left: 40,
    top: 40,
    fill: 'blue'
  })

  canvasB.add(rect, circle, triangle, txt);

  canvasB.calcOffset();
  canvasB.renderAll();
}



let canvasB_data = {
  "version": "4.3.0",
  "objects": [{
      "type": "image",
      "version": "4.3.0",
      "originX": "left",
      "originY": "top",
      "left": 62.5,
      "top": 0,
      "width": 400,
      "height": 600,
      "fill": "rgb(0,0,0)",
      "stroke": null,
      "strokeWidth": 0,
      "strokeDashArray": null,
      "strokeLineCap": "butt",
      "strokeDashOffset": 0,
      "strokeLineJoin": "miter",
      "strokeMiterLimit": 4,
      "scaleX": 0.25,
      "scaleY": 0.25,
      "angle": 0,
      "flipX": false,
      "flipY": false,
      "opacity": 1,
      "shadow": null,
      "visible": true,
      "backgroundColor": "",
      "fillRule": "nonzero",
      "paintFirst": "fill",
      "globalCompositeOperation": "source-over",
      "skewX": 0,
      "skewY": 0,
      "cropX": 0,
      "cropY": 0,
      "src": "https://placekitten.com/400/600",
      "crossOrigin": null,
      "filters": []
    },
    {
      "type": "rect",
      "version": "4.3.0",
      "originX": "left",
      "originY": "top",
      "left": 0,
      "top": 0,
      "width": 60,
      "height": 60,
      "fill": "orange",
      "stroke": null,
      "strokeWidth": 1,
      "strokeDashArray": null,
      "strokeLineCap": "butt",
      "strokeDashOffset": 0,
      "strokeLineJoin": "miter",
      "strokeMiterLimit": 4,
      "scaleX": 1,
      "scaleY": 1,
      "angle": 0,
      "flipX": false,
      "flipY": false,
      "opacity": 1,
      "shadow": null,
      "visible": true,
      "backgroundColor": "",
      "fillRule": "nonzero",
      "paintFirst": "fill",
      "globalCompositeOperation": "source-over",
      "skewX": 0,
      "skewY": 0,
      "rx": 0,
      "ry": 0
    },
    {
      "type": "circle",
      "version": "4.3.0",
      "originX": "left",
      "originY": "top",
      "left": 61.52,
      "top": 24.21,
      "width": 80,
      "height": 80,
      "fill": null,
      "stroke": "red",
      "strokeWidth": 12,
      "strokeDashArray": null,
      "strokeLineCap": "butt",
      "strokeDashOffset": 0,
      "strokeLineJoin": "miter",
      "strokeMiterLimit": 4,
      "scaleX": 1.1,
      "scaleY": 1.1,
      "angle": 0,
      "flipX": false,
      "flipY": false,
      "opacity": 1,
      "shadow": null,
      "visible": true,
      "backgroundColor": "",
      "fillRule": "nonzero",
      "paintFirst": "fill",
      "globalCompositeOperation": "source-over",
      "skewX": 0,
      "skewY": 0,
      "radius": 40,
      "startAngle": 0,
      "endAngle": 6.283185307179586
    },
    {
      "type": "triangle",
      "version": "4.3.0",
      "originX": "left",
      "originY": "top",
      "left": 163,
      "top": 107.89,
      "width": 40,
      "height": 40,
      "fill": "black",
      "stroke": null,
      "strokeWidth": 1,
      "strokeDashArray": null,
      "strokeLineCap": "butt",
      "strokeDashOffset": 0,
      "strokeLineJoin": "miter",
      "strokeMiterLimit": 4,
      "scaleX": 1,
      "scaleY": 1,
      "angle": 0,
      "flipX": false,
      "flipY": false,
      "opacity": 1,
      "shadow": null,
      "visible": true,
      "backgroundColor": "",
      "fillRule": "nonzero",
      "paintFirst": "fill",
      "globalCompositeOperation": "source-over",
      "skewX": 0,
      "skewY": 0
    },
    {
      "type": "text",
      "version": "4.3.0",
      "originX": "left",
      "originY": "top",
      "left": 2.33,
      "top": 39.01,
      "width": 162.216796875,
      "height": 22.599999999999998,
      "fill": "blue",
      "stroke": null,
      "strokeWidth": 1,
      "strokeDashArray": null,
      "strokeLineCap": "butt",
      "strokeDashOffset": 0,
      "strokeLineJoin": "miter",
      "strokeMiterLimit": 4,
      "scaleX": 1,
      "scaleY": 1,
      "angle": 0,
      "flipX": false,
      "flipY": false,
      "opacity": 1,
      "shadow": null,
      "visible": true,
      "backgroundColor": "",
      "fillRule": "nonzero",
      "paintFirst": "fill",
      "globalCompositeOperation": "source-over",
      "skewX": 0,
      "skewY": 0,
      "text": "Canvas B 225 x 150",
      "fontSize": 20,
      "fontWeight": "normal",
      "fontFamily": "Times New Roman",
      "fontStyle": "normal",
      "lineHeight": 1.16,
      "underline": false,
      "overline": false,
      "linethrough": false,
      "textAlign": "left",
      "textBackgroundColor": "",
      "charSpacing": 0,
      "styles": {}
    }
  ],
  "background": "#FFFFFF",
  "width": 225,
  "height": 150
}

let canvasA_data = {
  "version": "4.3.0",
  "objects": [{
      "type": "image",
      "version": "4.3.0",
      "originX": "left",
      "originY": "top",
      "left": 17.5,
      "top": 0,
      "width": 600,
      "height": 400,
      "fill": "rgb(0,0,0)",
      "stroke": null,
      "strokeWidth": 0,
      "strokeDashArray": null,
      "strokeLineCap": "butt",
      "strokeDashOffset": 0,
      "strokeLineJoin": "miter",
      "strokeMiterLimit": 4,
      "scaleX": 0.47,
      "scaleY": 0.47,
      "angle": 0,
      "flipX": false,
      "flipY": false,
      "opacity": 1,
      "shadow": null,
      "visible": true,
      "backgroundColor": "",
      "fillRule": "nonzero",
      "paintFirst": "fill",
      "globalCompositeOperation": "source-over",
      "skewX": 0,
      "skewY": 0,
      "cropX": 0,
      "cropY": 0,
      "src": "https://placekitten.com/600/400",
      "crossOrigin": null,
      "filters": []
    },
    {
      "type": "rect",
      "version": "4.3.0",
      "originX": "left",
      "originY": "top",
      "left": 277.89,
      "top": 73.23,
      "width": 20,
      "height": 20,
      "fill": "red",
      "stroke": null,
      "strokeWidth": 1,
      "strokeDashArray": null,
      "strokeLineCap": "butt",
      "strokeDashOffset": 0,
      "strokeLineJoin": "miter",
      "strokeMiterLimit": 4,
      "scaleX": 1,
      "scaleY": 1,
      "angle": 0,
      "flipX": false,
      "flipY": false,
      "opacity": 1,
      "shadow": null,
      "visible": true,
      "backgroundColor": "",
      "fillRule": "nonzero",
      "paintFirst": "fill",
      "globalCompositeOperation": "source-over",
      "skewX": 0,
      "skewY": 0,
      "rx": 0,
      "ry": 0
    },
    {
      "type": "circle",
      "version": "4.3.0",
      "originX": "left",
      "originY": "top",
      "left": 92.67,
      "top": -5.57,
      "width": 40,
      "height": 40,
      "fill": null,
      "stroke": "green",
      "strokeWidth": 12,
      "strokeDashArray": null,
      "strokeLineCap": "butt",
      "strokeDashOffset": 0,
      "strokeLineJoin": "miter",
      "strokeMiterLimit": 4,
      "scaleX": 1.73,
      "scaleY": 1.73,
      "angle": 0,
      "flipX": false,
      "flipY": false,
      "opacity": 1,
      "shadow": null,
      "visible": true,
      "backgroundColor": "",
      "fillRule": "nonzero",
      "paintFirst": "fill",
      "globalCompositeOperation": "source-over",
      "skewX": 0,
      "skewY": 0,
      "radius": 20,
      "startAngle": 0,
      "endAngle": 6.283185307179586
    },
    {
      "type": "triangle",
      "version": "4.3.0",
      "originX": "left",
      "originY": "top",
      "left": 17.2,
      "top": 146.93,
      "width": 40,
      "height": 40,
      "fill": "blue",
      "stroke": null,
      "strokeWidth": 1,
      "strokeDashArray": null,
      "strokeLineCap": "butt",
      "strokeDashOffset": 0,
      "strokeLineJoin": "miter",
      "strokeMiterLimit": 4,
      "scaleX": 1,
      "scaleY": 1,
      "angle": 0,
      "flipX": false,
      "flipY": false,
      "opacity": 1,
      "shadow": null,
      "visible": true,
      "backgroundColor": "",
      "fillRule": "nonzero",
      "paintFirst": "fill",
      "globalCompositeOperation": "source-over",
      "skewX": 0,
      "skewY": 0
    },
    {
      "type": "text",
      "version": "4.3.0",
      "originX": "left",
      "originY": "top",
      "left": 51.99,
      "top": 96.51,
      "width": 195.984375,
      "height": 27.119999999999994,
      "fill": "white",
      "stroke": null,
      "strokeWidth": 1,
      "strokeDashArray": null,
      "strokeLineCap": "butt",
      "strokeDashOffset": 0,
      "strokeLineJoin": "miter",
      "strokeMiterLimit": 4,
      "scaleX": 1,
      "scaleY": 1,
      "angle": 0,
      "flipX": false,
      "flipY": false,
      "opacity": 1,
      "shadow": null,
      "visible": true,
      "backgroundColor": "",
      "fillRule": "nonzero",
      "paintFirst": "fill",
      "globalCompositeOperation": "source-over",
      "skewX": 0,
      "skewY": 0,
      "text": "Canvas A 320 x 190",
      "fontSize": 24,
      "fontWeight": "normal",
      "fontFamily": "Times New Roman",
      "fontStyle": "normal",
      "lineHeight": 1.16,
      "underline": false,
      "overline": false,
      "linethrough": false,
      "textAlign": "left",
      "textBackgroundColor": "",
      "charSpacing": 0,
      "styles": {}
    }
  ],
  "background": "#FFFFFF",
  "width": 320,
  "height": 190
}
#container {
  display: grid;
  grid-template-columns: 330px 235px;
  gap: 20px;
  grid-template-rows: 300px, 20px, 20px, 100px;
}

.canvas-wrapper {
  margin-left: 10px;
  grid-column: 1;
  justify-self: center;
}

.panel {
  grid-column: 2;
  margin-top: 20px;
}

.buttons {
  font-family: sans-serif;
  font-size: 10pt;
  width: 100px;
  margin: 3px 0;
}

.labels {
  font-family: sans-serif;
  font-size: 12pt;
}
<!DOCTYPE html>
<html>

<head>
  <title></title>

  <script type="text/javascript" src="https://cdn.jsdelivr.net/npm/fabric@4.3.0/dist/fabric.js"></script>
  <script type="text/javascript" src="js/fabric-resizing.js" defer></script>

  <link rel="stylesheet" href="css/fabric-resize.css">
</head>

<body>
  <div id="container" width="100%" height="100%">
    <div class="canvas-wrapper">
      <div class="labels">Canvas A - 320 x 190 px</div>
      <div>
        <canvas id="canvas-A" width="320" height="200" style="border:1px solid #000000;"></canvas>
      </div>
    </div>
    <div class="panel">
      <textarea name="canA" id="canA" cols="23" rows="8"></textarea>
      <div>
        <button id="canA-loadB" class="buttons">Load B Json</button>
        <button id="canA-loadA" class="buttons">Load A Json</button>
      </div>
      <div>
        <button id="canA-clear" class="buttons">Clear A</button>
        <button id="canA-save" class="buttons">Save A Json</button>
      </div>
    </div>
    <div class="canvas-wrapper">
      <div class="labels">Canvas B - 225 x 150 px</div>
      <canvas id="canvas-B" width="225" height="150" style="border:1px solid #000000;"></canvas>
    </div>
    <div class="panel">
      <textarea name="canB" id="canB" cols="23" rows="8"></textarea>
      <div>
        <button id="canB-loadA" class="buttons">Load A Json</button>
        <button id="canB-loadB" class="buttons">Load B Json</button>
      </div>
      <div>
        <button id="canB-clear" class="buttons">Clear B</button>
        <button id="canB-save" class="buttons">Save B Json</button>
      </div>
    </div>
    <div class="labels" style="position:fixed; left:50px; top:455px;">
      600 x 400 px
    </div>
    <div>
      <img id="test-imageA" src="https://placekitten.com/600/400" style="position:fixed; left:50px; top:475px; width:25%; border:none;" />
    </div>
    <div class="labels" style="position:fixed; left:250px; top:455px;">
      400 x 600 px
    </div>
    <div>
      <img id="test-imageB" src="https://placekitten.com/400/600" style="position:fixed; left:250px; top:475px; height:25%; border:none;" />
    </div>
  </div>
</body>

</html>


Solution

  • Finally solved this after trying many different approaches (and much anguish).

    The gist is to select all objects and then scale and center. A tricky thing is that the first object in my data is an image which all the drawn elements need to stay in register with. So I first correct the image path and then, after the selection has been scaled, I shift the selection to that the image remains centered on-screen.

    Looking at the code now, I can see places to optimize it (e.g. since there is only ever one image in my data, there is no need to loop through all the remaining data after the images has been found)


    loadData(data) {
    
        var jsonObj = JSON.parse(data);
    
        jsonObj.objects.forEach(element => {
          if (element.type == "image") {
            var imgPath = element.src.split("assets")
    
            if (imgPath.length > 1) {
              element.src = upath.joinSafe(projectDirectory, "assets", imgPath[1])
            }
          }
        });
    
        var self = this;
        this.canvas.loadFromJSON(jsonObj, function () {
    
          var selection = new fabric.ActiveSelection(self.canvas.getObjects(), { canvas: self.canvas });
    
    
          // var selectionWidth = (selection.width >  self.canvas.getWidth()) ?  self.canvas.getWidth() : selection.width
          // var selectionHeight = (selection.height > self.canvas.getHeight()) ? self.canvas.getHeight() : selection.height
    
          var sizeObj = self.resizer(
            { width: self.canvas.getWidth(), height: self.canvas.getHeight() },
            { width: selection.width, height: selection.height });
    
          // console.log('sizeObj', sizeObj);
    
          // selection.scaleToWidth(sizeObj.width, false)
          // selection.scaleToWidth(sizeObj.width, true)
          selection.scaleToHeight(sizeObj.height)
          // selection.scaleToHeight(sizeObj.height, true)
    
          selection.center();
    
    
          /**
           * ------------------------------------------
           * This keeps the IMAGE centered on the canvas instead of 
           * just centering  the selection – otherwise the image will shift 
           */
          var selectionObjs = selection.getObjects();
          var imgObj = selectionObjs[0]
          var matrix = selection.calcTransformMatrix();
          var finalPosition = fabric.util.transformPoint({ x: imgObj.left, y: imgObj.top }, matrix);
    
          sizeObj = self.resizer(
            { width: self.canvas.getWidth(), height: self.canvas.getHeight() },
            { width: imgObj.getScaledWidth(), height: imgObj.getScaledHeight() });
    
          selection.left += sizeObj.x - finalPosition.x;
          selection.top += sizeObj.y - finalPosition.y;
          // ------------------------------------------
    
          selection.setCoords()
          selection.destroy()
    
          self.canvas.renderAll();
          self.canvas.calcOffset()
    
        }, function (o, object) {
    
        })
    
        self.setObjectsSelectable(self.toolbarIsVisible)
        self.toolActive = false
      }
    

    resizer(canvas, imageObj) {
        var imageAspectRatio = imageObj.width / imageObj.height;
        var canvasAspectRatio = canvas.width / canvas.height;
        var renderableHeight, renderableWidth, xStart, yStart;
    
        // If image's aspect ratio is less than canvas's we fit on height
        // and place the image centrally along width
        if (imageAspectRatio < canvasAspectRatio) {
          renderableHeight = canvas.height;
          renderableWidth = imageObj.width * (renderableHeight / imageObj.height);
          xStart = (canvas.width - renderableWidth) / 2;
          yStart = 0;
        }
    
        // If image's aspect ratio is greater than canvas's we fit on width
        // and place the image centrally along height
        else if (imageAspectRatio > canvasAspectRatio) {
          renderableWidth = canvas.width
          renderableHeight = imageObj.height * (renderableWidth / imageObj.width);
          xStart = 0;
          yStart = (canvas.height - renderableHeight) / 2;
        }
    
        // Happy path - keep aspect ratio
        else {
          renderableHeight = canvas.height;
          renderableWidth = canvas.width;
          xStart = 0;
          yStart = 0;
        }
        return { x: xStart, y: yStart, width: renderableWidth, height: renderableHeight }
      }