6

I'm using FabricJS for a project in which I need to be able to set the background image of the canvas. The canvas can be any size and can be resized at any time. Lastly, the image should always fill the canvas regardless of its size, but never be distorted in any way.

So for example, if I have an 800x600 (WxH) canvas and a 1200x700 background image, the image should be scaled down to 1029x600 so that it covers the entire canvas without being distorted.

I've written a function that is supposed to calculate the dimensions of the canvas and image and set the size and position of the image accordingly, but I'm having trouble getting it to work correctly. It works most of the time, but is not "bullet proof". Sometimes the image gets distorted and other times it doesn't fill the entire canvas.

What I would like help with is refactoring this into a bullet-proof solution that will always meet my sizing criteria no matter what size the canvas or image is.

I've created a fiddle with the code. the fiddle loads an 800x600 canvas and then sets first a landscape background image to demonstrate how the code isn't sizing the image to cover the canvas, and then a portrait image to show how it sometimes distorts images.

Here is the code itself:

var canvas = window._canvas = new fabric.Canvas('c'),
    canvasOriginalWidth = 800,
    canvasOriginalHeight = 600,
    canvasWidth = 800,
    canvasHeight = 600,
    canvasScale = .5,
    photoUrlLandscape = 'https://images8.alphacoders.com/292/292379.jpg',
    photoUrlPortrait = 'https://presspack.rte.ie/wp-content/blogs.dir/2/files/2015/04/AMC_TWD_Maggie_Portraits_4817_V1.jpg';

setCanvasSize({height: canvasHeight, width: canvasWidth});
setTimeout(function() {
    setCanvasBackgroundImageUrl(photoUrlLandscape, 0, 0, 1)
}, 50)
setTimeout(function() {
    setCanvasBackgroundImageUrl(photoUrlPortrait, 0, 0, 1)
}, 4000)

function setCanvasSize(canvasSizeObject) {
    canvas.setWidth(canvasSizeObject.width);
    canvas.setHeight(canvasSizeObject.height);
    setZoom();
}
function setZoom() {
    setCanvasZoom();
    canvas.renderAll();
}
function setCanvasZoom() {
    var width = canvasOriginalWidth;
    var height = canvasOriginalHeight;
    var tempWidth = width * canvasScale;
    var tempHeight = height * canvasScale;

    canvas.setWidth(tempWidth);
    canvas.setHeight(tempHeight);
}
function setCanvasBackgroundImageUrl(url, top, left, opacity) {
    if(url && url.length > 0) {
        fabric.Image.fromURL(url, function(img) {
            var aspect, scale;

            if(parseInt(canvasWidth) > parseInt(canvasHeight)) {
                if(img.width >= img.height) {
                    // Landscape canvas, landscape source photo
                    aspect = img.width / img.height;

                    if(img.width >= parseInt(canvasWidth)) {
                        scale = img.width / parseInt(canvasWidth);
                    } else {
                        scale = parseInt(canvasWidth) / img.width;
                    }

                    img.width = parseInt(canvasWidth)
                    img.height = img.height / scale;
                } else {
                    // Landscape canvas, portrait source photo
                    aspect = img.height / img.width;

                    if(img.width >= parseInt(canvasWidth)) {
                        scale = img.width / parseInt(canvasWidth);
                    } else {
                        scale = parseInt(canvasWidth) / img.width;
                    }

                    img.width = parseInt(canvasWidth);
                    img.height = img.height * scale;
                }
            } else {
                if(img.width >= img.height) {
                    // Portrait canvas, landscape source photo
                    aspect = img.width / img.height;

                    if(img.height >= parseInt(canvasHeight)) {
                        scale = img.width / parseInt(canvasHeight);
                    } else {
                        scale = parseInt(canvasHeight) / img.height;
                    }

                    img.width = img.width * scale;
                    img.height = parseInt(canvasHeight)
                } else {
                    // Portrait canvas, portrait source photo
                    aspect = img.height / img.width;

                    if(img.height >= parseInt(canvasHeight)) {
                        scale = img.height / parseInt(canvasHeight);
                    } else {
                        scale = parseInt(canvasHeight) / img.height;
                    }

                    img.width = img.width * scale;
                    img.height = parseInt(canvasHeight);
                }
            }

            canvas.setBackgroundImage(img, canvas.renderAll.bind(canvas), {
                top: parseInt(top) || 0,
                left: parseInt(left) || 0,
                originX: 'left',
                originY: 'top',
                opacity: opacity ? opacity : 1,
                scaleX: canvasScale,
                scaleY: canvasScale
            });

            canvas.renderAll();
            setZoom();
        });
    } else {
        canvas.backgroundImage = 0;
        canvas.setBackgroundImage('', canvas.renderAll.bind(canvas));

        canvas.renderAll();
        setZoom();
    }
};
Daniel Bonnell
  • 4,817
  • 9
  • 48
  • 88
  • You say that you want to preserve the aspect ratio of the image, and that you want it to fill the canvas, but what is the desired behaviour if the proportions of the image don't match those of the canvas? Do you want the image cropped and centered on the canvas? – John M Nov 09 '16 at 14:23
  • Yes, the image should be cropped and centered (if possible) if there is overlap. I'm not as concerned with centering since my app allows a user to move the background image around. – Daniel Bonnell Nov 09 '16 at 18:36

2 Answers2

17

Here is a fiddle that does what (I think) you want to achieve:

https://jsfiddle.net/whippet71/7s5obuk2/

The code for scaling the image is fairly straightforward:

    function scaleAndPositionImage() {
        setCanvasZoom();

        var canvasAspect = canvasWidth / canvasHeight;
        var imgAspect = bgImage.width / bgImage.height;
        var left, top, scaleFactor;

        if (canvasAspect >= imgAspect) {
            var scaleFactor = canvasWidth / bgImage.width;
            left = 0;
            top = -((bgImage.height * scaleFactor) - canvasHeight) / 2;
        } else {
            var scaleFactor = canvasHeight / bgImage.height;
            top = 0;
            left = -((bgImage.width * scaleFactor) - canvasWidth) / 2;

        }

        canvas.setBackgroundImage(bgImage, canvas.renderAll.bind(canvas), {
            top: top,
            left: left,
            originX: 'left',
            originY: 'top',
            scaleX: scaleFactor,
            scaleY: scaleFactor
        });
        canvas.renderAll();

    }

Basically you just want to know if the aspect ratio of the image is greater or less than that of the canvas. Once you know that you can work out the scale factor, the final step is to work out how to offset the image such that it's centered on the canvas.

John M
  • 2,510
  • 6
  • 23
  • 31
  • 1
    So much more elegant than what I was trying. I was trying to account for every possibility individually. Seeing your solution in code helps make sense of this. This is great! Thanks! – Daniel Bonnell Nov 09 '16 at 23:48
5
fabric.Image.fromURL("image.jpg", function (img) {    
    canvas.setBackgroundImage(img, canvas.renderAll.bind(canvas), {
        scaleX: canvas.width / img.width,
        scaleY: canvas.height / img.height
    });
});

It set image to full canvas

Jay Kukadiya
  • 553
  • 6
  • 9