Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

Tags:

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 = '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.

like image 515
Lucio Avatar asked Jul 25 '17 10:07

Lucio


2 Answers

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.

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

like image 159
Jorge Fuentes González Avatar answered Sep 22 '22 04:09

Jorge Fuentes González


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 (250x500 instead of 750x1500) 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.

like image 23
dib258 Avatar answered Sep 19 '22 04:09

dib258