Canvas drawing takes a lot of time on Safari but not on Chrome or FF

I am making a kaleidoscope on my website. All it does is take an image (either via Drag & Drop or a default image on load) and copy it 10 times (one for each slice of the kaleidoscope). On mouse move, the rotation and scale of the slices are adjusted to achieve the desired effect.

On Google Chrome and Firefox, it works seamlessly, without any lag. However, on Safari the website is unusable as it is too slow. Am I missing something?

Here is a JSFiddle showing the problem. Please note I already tried replacing setTimeout(update, 1000 / 60) with RequestAnimationFrame, without any improvements.

JSFiddle: Link

$(document).ready(function () {
    //SCRIPT KALEIDOSCOPE BASE

    var DragDrop, Kaleidoscope, c, dragger, gui, i, image, kaleidoscope, len, onChange, onMouseMoved, options, ref, tr, tx, ty, update,
        bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };

    Kaleidoscope = (function() {
        Kaleidoscope.prototype.HALF_PI = Math.PI / 2;

        Kaleidoscope.prototype.TWO_PI = Math.PI * 2;

        var optimal_radius = window.innerHeight;

        if (window.innerWidth > optimal_radius) {
            optimal_radius = window.innerWidth;
        }

        function Kaleidoscope(options1) {
            var key, ref, ref1, val;
            this.options = options1 != null ? options1 : {};
            this.defaults = {
                offsetRotation: 0.0,
                offsetScale: 1.0,
                offsetX: 0.0,
                offsetY: 0.0,
                radius: optimal_radius / 1.4,
                slices: 12,
                zoom: 1.0
            };
            ref = this.defaults;
            for (key in ref) {
                val = ref[key];
                this[key] = val;
            }
            ref1 = this.options;
            for (key in ref1) {
                val = ref1[key];
                this[key] = val;
            }
            if (this.domElement == null) {
                this.domElement = document.getElementById('kaleidoscope');
            }
            if (this.context == null) {
                this.context = this.domElement.getContext('2d');
            }
            if (this.image == null) {
                this.image = document.createElement('img');
            }
        }

        Kaleidoscope.prototype.draw = function() {
            var cx, i, index, ref, results, scale, step;
            this.domElement.width = this.domElement.height = this.radius * 2;
            this.context.fillStyle = this.context.createPattern(this.image, 'repeat');
            scale = this.zoom * (this.radius / Math.min(this.image.width, this.image.height));
            step = this.TWO_PI / this.slices;
            cx = this.image.width / 2;
            results = [];
            for (index = i = 0, ref = this.slices; 0 <= ref ? i <= ref : i >= ref; index = 0 <= ref ? ++i : --i) {
                this.context.save();
                this.context.translate(this.radius, this.radius);
                this.context.rotate(index * step);
                this.context.beginPath();
                this.context.moveTo(-0.5, -0.5);
                this.context.arc(0, 0, this.radius, step * -0.51, step * 0.51);
                this.context.lineTo(0.5, 0.5);
                this.context.closePath();
                this.context.rotate(this.HALF_PI);
                this.context.scale(scale, scale);
                this.context.scale([-1, 1][index % 2], 1);
                this.context.translate(this.offsetX - cx, this.offsetY);
                this.context.rotate(this.offsetRotation);
                this.context.scale(this.offsetScale, this.offsetScale);
                this.context.fill();
                results.push(this.context.restore());
            }
            return results;
        };

        return Kaleidoscope;

    })();

    DragDrop = (function() {
        function DragDrop(callback, context, filter) {
            var disable;
            this.callback = callback;
            this.context = context != null ? context : document;
            this.filter = filter != null ? filter : /^image/i;
            this.onDrop = bind(this.onDrop, this);
            disable = function(event) {
                event.stopPropagation();
                return event.preventDefault();
            };
            this.context.addEventListener('dragleave', disable);
            this.context.addEventListener('dragenter', disable);
            this.context.addEventListener('dragover', disable);
            this.context.addEventListener('drop', this.onDrop, false);
        }

        DragDrop.prototype.onDrop = function(event) {
            var file, reader;
            event.stopPropagation();
            event.preventDefault();
            file = event.dataTransfer.files[0];
            if (this.filter.test(file.type)) {
                reader = new FileReader;
                reader.onload = (function(_this) {
                    return function(event) {
                        return typeof _this.callback === "function" ? _this.callback(event.target.result) : void 0;
                    };
                })(this);
                return reader.readAsDataURL(file);
            }
        };

        return DragDrop;

    })();

    image = new Image;

    image.onload = (function(_this) {
        return function() {
            return kaleidoscope.draw();
        };
    })(this);

    image.src="https://stackoverflow.com/questions/45300903/img/kaleidoscope.jpg";

    kaleidoscope = new Kaleidoscope({
        image: image,
        slices: 10
    });

    kaleidoscope.domElement.style.position = 'absolute';

    kaleidoscope.domElement.style.marginLeft = -kaleidoscope.radius + 'px';

    kaleidoscope.domElement.style.marginTop = -kaleidoscope.radius + 'px';

    kaleidoscope.domElement.style.left="50%";

    kaleidoscope.domElement.style.top = '50%';

    document.getElementsByTagName('header')[0].appendChild(kaleidoscope.domElement);

    dragger = new DragDrop(function(data) {
        return kaleidoscope.image.src = data;
    });

    tx = kaleidoscope.offsetX;

    ty = kaleidoscope.offsetY;

    tr = kaleidoscope.offsetRotation;

    onMouseMoved = (function(_this) {
        return function(event) {
            var cx, cy, dx, dy, hx, hy;
            cx = window.innerWidth / 10;
            cy = window.innerHeight / 10;
            dx = event.pageX / window.innerWidth;
            dy = event.pageY / window.innerHeight;
            hx = dx - 0.5;
            hy = dy - 0.5;
            tx = hx * kaleidoscope.radius * -2;
            ty = hy * kaleidoscope.radius * 2;
            return tr = Math.atan2(hy, hx);
        };
    })(this);

    window.addEventListener('mousemove', onMouseMoved, false);

    options = {
        interactive: true,
        ease: 0.1
    };

    (update = (function(_this) {
        return function() {
            var delta, theta;
            if (options.interactive) {
                delta = tr - kaleidoscope.offsetRotation;
                theta = Math.atan2(Math.sin(delta), Math.cos(delta));
                kaleidoscope.offsetX += (tx - kaleidoscope.offsetX) * options.ease;
                kaleidoscope.offsetY += (ty - kaleidoscope.offsetY) * options.ease;
                kaleidoscope.offsetRotation += (theta - kaleidoscope.offsetRotation) * options.ease;
                kaleidoscope.draw();
            }
            return setTimeout(update, 1000 / 60);
        };
    })(this))();

    onChange = (function(_this) {
        return function() {
            kaleidoscope.domElement.style.marginLeft = -kaleidoscope.radius + 'px';
            kaleidoscope.domElement.style.marginTop = -kaleidoscope.radius + 'px';
            options.interactive = false;
            return kaleidoscope.draw();
        };
    })(this);
});

From what I saw, the problem occurs only when the canvas is in full screen. If it shows up in a small space, it works seamlessly. However, on my website, it will be fullscreen.

Read More:   Write results into a file using CasperJS

Regarding all the optimisation made to your code, and the fact that on safari it still has a framerate near zero. I tried modifying the picture you use, to reduce the size(jpg quality 60, 30, 10), change image format (png24, png8), change the size of the picture (250×500 instead of 750×1500) and all of those changes changed nothing. Still lagging a lot.

I then tried to find some benchmarks made with Safari Canvas. I found this chart which is showing that the performances from Safari with canvas are not the best.

Rend times, varying drawing area height

You can see the full benchmark article here

I think in the end, even after the optimization made by @Jorge Fuentes González, your code is still rendering slow on Safari then maybe there is a reason and it’s in the core of the Webkit engine.

Woah! The main problem you have is that you are drawing a HUGE canvas. You are creating a canvas WAY bigger than the window size. Although part of the canvas is not shown, the calculations to draw on that area are done anyway. You only have to draw the pixels that can be viewed.

Here you can see your actual canvas size: http://i.imgur.com/trOYlcV.png

With this and @Kaiido tips I created this fiddle: https://jsfiddle.net/Llorx/sd1skrj8/9/

My canvas size: http://i.imgur.com/4BzmCqh.png

I simply created a canvas filling the viewport and draw inside it increasing the arc radius, being the canvas the one limiting the pixels “viewport”, and not the window.

Changed:

this.context.arc(0, 0, this.radius, step * -0.51, step * 0.51);
// [...]
kaleidoscope.domElement.style.marginLeft = -kaleidoscope.radius + 'px';
kaleidoscope.domElement.style.marginTop = -kaleidoscope.radius + 'px';
kaleidoscope.domElement.style.left="50%";
kaleidoscope.domElement.style.top = '50%';

for

this.context.arc(0, 0, this.radius*1.5, step * -0.51, step * 0.51);
// [...]
kaleidoscope.domElement.style.width = "100vw";
kaleidoscope.domElement.style.height = "100vh";
kaleidoscope.domElement.style.left = 0;
kaleidoscope.domElement.style.top = 0;

This can be improved to have an actual circle when screen ratio is not square, and such, but you get the idea: Never make the canvas bigger than needed.

Read More:   Override function in JavaScript [duplicate]

PD: Don’t have Safari to test. Tell me if this improves performance.

Interesting defect, I would remove the css styling on the canvas as @Jorge suggests, then I would render the effect on an off-screen canvas, and then copy the rendered frame to the visible canvas. The DOM renderer will then not need to worry about off-screen clipping.


The answers/resolutions are collected from stackoverflow, are licensed under cc by-sa 2.5 , cc by-sa 3.0 and cc by-sa 4.0 .

Similar Posts