4
\$\begingroup\$

I'm working on a little project where I have to create sort of an image mask. Overall my code works quite good in all browsers but sporadically the animations "flickr" on Chrome (Windows). So I was wondering if I could increase to performance when repainting my canvas (on mouseover/out).

DEMO

JS:

// Stores all dday objects
var Ddays = {};

// Configuration options
var radiusGap = 10,
    radiusSteps = 9,
    imageLeftOffsetLeft = 200;

var Dday = function(index, canvas) {
    this.ctx = canvas.getContext('2d');
    this.$canvas = $(canvas);

    // Store relevant information on object
    this.ddayClasses = this.$canvas.parent().attr('class');
    this.sectorColor = this.$canvas.parent().css('background-color');
    this.width = this.$canvas.width();
    this.height = this.$canvas.height();
    this.animationMode = '';
    this.isSold = this.ddayClasses.indexOf('sold') !== -1 ? true : false;

    // We need to store the current object in a superior object
    // to access it on mouseover/out
    this.$canvas.attr('data-id', index);
    Ddays[index] = this;

    if(!this.isSold) {
        this.setRadiuses();
    }

    var _self = this;

    $('<img src="'+ _self.$canvas.data('img') +'">').load(function() {
        _self.image = this;

        // Scale image to fit height
        if(this.height > _self.height) {
            _self.imageHeight = _self.height - radiusGap * 3;
            _self.imageWidth = this.width - (this.height - _self.height) - radiusGap * 3;
        } else {
            _self.imageWidth = this.width;
            _self.imageHeight = this.height;
        }

        _self.setPositioning(_self);
        _self.drawStage(_self);

        if(!_self.isSold) {
            _self.$canvas.on('mouseover mouseout', _self.animate);
        }
    });

}

/********************************************************
         Calculate and store x/y positioning
********************************************************/

Dday.prototype.setPositioning = function(_self) {
    var positioning = {}

    if(_self.ddayClasses.indexOf('dday-big') !== -1 ||
       _self.ddayClasses.indexOf('dday-listview') !== -1) {

        positioning.x = imageLeftOffsetLeft;
        positioning.imgX = positioning.x - _self.imageWidth / 2;
    } else {
        positioning.x = _self.width / 2;
        positioning.imgX = positioning.x - _self.imageWidth / 2;
    }

    positioning.y = _self.height / 2;
    positioning.imgY = positioning.y - _self.imageHeight / 2;

    _self.positioning = positioning;

};

/********************************************************
         Calculate and store radii of object depending on the given p* class
********************************************************/

Dday.prototype.setRadiuses = function() {
    var pSize = this.ddayClasses.match(/[p.][0-9][0-9]/)[0].substr(1);

    this.maxRadius = this.height / 2 - radiusGap;
    this.initialRadius = this.radius = (this.maxRadius / radiusSteps) * (pSize / 10);
};      

/********************************************************
         Mouseover/out callback to initialize animation
********************************************************/

Dday.prototype.animate = function(ev) {
    var _self = Ddays[$(this).data('id')];

    _self.animationMode = ev.type;

    requestAnimationFrame(function () {
        _self.drawStage(_self);
    });
};

Dday.prototype.drawStage = function(_self) {    
    /********************************************************
             Draw background image, sector color overlay and 
             circle background
    ********************************************************/

    _self.ctx.fillStyle = '#fff';
    _self.ctx.fillRect(0, 0, _self.width, _self.height);                

    _self.ctx.drawImage(_self.image, _self.positioning.imgX, _self.positioning.imgY, _self.imageWidth, _self.imageHeight);

    _self.ctx.fillStyle = _self.sectorColor;
    _self.ctx.fillRect(0, 0, _self.width, _self.height);

    // We don't need the stuff bellow when the item is sold
    if(_self.isSold) {
        return;
    }                   

    _self.ctx.fillStyle = '#fff';
    _self.ctx.beginPath();
    _self.ctx.arc(_self.positioning.x, _self.positioning.y, _self.radius, 0, 2 * Math.PI, true);
    _self.ctx.closePath();
    _self.ctx.fill();               

    /********************************************************
             Setup new canvas which we later append back to the existing.
             We have to do this because when using clip() for the image
             the arc won't render antialised in Chrome (crispy borders):
             http://stackoverflow.com/questions/9536257/how-to-anti-alias-clip-edges-in-html5-canvas-under-chrome-windows/13854185#answer-13854185
    ********************************************************/

    var scratchCanvas = document.createElement('canvas');
    scratchCanvas.width = _self.width;
    scratchCanvas.height = _self.height;
    var scratchCtx = scratchCanvas.getContext('2d');


    /********************************************************
             Clear stage and draw image with standard composite operation
    ********************************************************/       

    scratchCtx.clearRect(0, 0, scratchCanvas.width, scratchCanvas.height);
    scratchCtx.globalCompositeOperation = 'source-over';
    scratchCtx.drawImage(_self.image, _self.positioning.imgX, _self.positioning.imgY, _self.imageWidth, _self.imageHeight);

    /********************************************************
             Render arc and clip image
             https://developer.mozilla.org/samples/canvas-tutorial/6_1_canvas_composite.html
    ********************************************************/       

    scratchCtx.fillStyle = '#fff';
    scratchCtx.globalCompositeOperation = 'destination-in';
    scratchCtx.beginPath();
    scratchCtx.arc(_self.positioning.x, _self.positioning.y, _self.radius, 0, 2 * Math.PI, true);
    scratchCtx.closePath();
    scratchCtx.fill();

    /********************************************************
             Append created canvas to the existing one
    ********************************************************/   

    _self.ctx.drawImage(scratchCanvas, 0, 0);

    /********************************************************
             Restart drawing when animationMode is set
     ********************************************************/  

    if(_self.animationMode === 'mouseover') {

        if(_self.radius < _self.maxRadius) {
            // Prevent that radius is higher than the maximal radius
            _self.radius = _self.radius * 1.15 >= _self.maxRadius ? _self.maxRadius : _self.radius * 1.15;

            requestAnimationFrame(function () {
                _self.drawStage(_self);
            });
        }

    } else if(_self.animationMode === 'mouseout') {

        if(_self.radius > _self.initialRadius) {
            // Prevent that radius is lower than the initial radius
            _self.radius = _self.radius * 0.9 <= _self.initialRadius ? _self.initialRadius : _self.radius * 0.9;

            requestAnimationFrame(function () {
                _self.drawStage(_self);
            });
        }

    } else {
        _self.animationMode = '';
    }
};

/********************************************************
         Setup new Objects
********************************************************/

$('.dday canvas').each(function(index, canvas){
    new Dday(index, canvas);
});

/********************************************************
         AnimationFrame polyfill
********************************************************/

(function() {
    var lastTime = 0;
    var vendors = ['webkit', 'moz'];
    for(var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) {
        window.requestAnimationFrame = window[vendors[x]+'RequestAnimationFrame'];
        window.cancelAnimationFrame =
            window[vendors[x]+'CancelAnimationFrame'] || window[vendors[x]+'CancelRequestAnimationFrame'];
    }

    if (!window.requestAnimationFrame)
        window.requestAnimationFrame = function(callback, element) {
            var currTime = new Date().getTime();
            var timeToCall = Math.max(0, 16 - (currTime - lastTime));
            var id = window.setTimeout(function() { callback(currTime + timeToCall); },
                                       timeToCall);
            lastTime = currTime + timeToCall;
            return id;
        };

    if (!window.cancelAnimationFrame)
        window.cancelAnimationFrame = function(id) {
            clearTimeout(id);
        };
}());

The HTML code is predefined and I can't make changes to it. I'd appreciate any suggestions on how to improve my code and especially the performance.

\$\endgroup\$
7
+50
\$\begingroup\$

I understand that this might not be actually what you want but I've recreated your effect using only CSS instead of canvas.

Although it employs some CSS magic, I think it's much simpler than using canvas for this, and you wont need to worry about performance.

div {
    width: 300px;
    height: 300px;
    position: relative;
    overflow: hidden;
    background: url(http://static.parastorage.com/media/bbb905_ed7c93d58f92418ba99d4bdaa9601be0.jpg_256) no-repeat center center;
}

div::before {
    content: '';
    position: absolute;
    bottom: 0;
    width: 40%;
    height: 40%;
    border-radius: 50%;
    box-shadow: 0px 200px 0px 390px rgba(247,99,62,0.8);
    left: 0;
    right: 0;
    margin: auto;
    top: 0;
    opacity: 0.85;
    transition: all .2s ease;
    -webkit-transition: all .2s ease;
}

div:hover::before {
    width: 80%;
    height: 80%;
}

http://jsfiddle.net/yefw4/4/

\$\endgroup\$
  • 4
    \$\begingroup\$ This is the best JavaScript performance tips: don't use JavaScript when you don't have to :) \$\endgroup\$ – pomeh Jul 16 '14 at 12:13
  • \$\begingroup\$ Great suggestion! I had something similar before but the client wants the same effect in IE9 (where the CSS transitions don't work). But anyway thanks for your response! +1 \$\endgroup\$ – damianfrizzi Jul 16 '14 at 12:35
  • \$\begingroup\$ @DamianFrizzi If you change the code to use real elements instead of pseudo elements then it will work on IE9. \$\endgroup\$ – Jivings Jul 16 '14 at 12:46
  • \$\begingroup\$ @konijn Sorted :) \$\endgroup\$ – Jivings Jul 16 '14 at 15:21
  • \$\begingroup\$ All that JS code written for nothing... Thank you for your answer! Next time I should think twice about how to realize something... :) \$\endgroup\$ – damianfrizzi Jul 17 '14 at 11:51
8
\$\begingroup\$

From what I understand, you don't need to draw the image every time the event mouseover/mouseout is triggered. The only thing that changes when the mouse moves is the radius of the colored circle. I mean the following code could be done only once on the initialization process:

_self.ctx.fillStyle = '#fff';
_self.ctx.fillRect(0, 0, _self.width, _self.height);                

_self.ctx.drawImage(_self.image, _self.positioning.imgX, _self.positioning.imgY, _self.imageWidth, _self.imageHeight);

In a general way with canvas, you have to paint only what's needed really, and not repaint all the canvas every time. So here you can create a first canvas with the image only, which would be painted on the initialization process, then a second canvas which handle the colored circle animation only. You display both in the same place and here you go.

Also, I think you don't need to create the scratchCanvas every time in the drawStage method:

var scratchCanvas = document.createElement('canvas');
scratchCanvas.width = _self.width;
scratchCanvas.height = _self.height;
var scratchCtx = scratchCanvas.getContext('2d');

Create it once during initialization, then retrieve it later.

Hope this will help.

\$\endgroup\$
  • \$\begingroup\$ Thank you for your suggestions. Seems logic to draw the these two things only on initialization. I choose Jivings`s answer though as it's a much higher performance enhancement. \$\endgroup\$ – damianfrizzi Jul 17 '14 at 11:53
  • 1
    \$\begingroup\$ @DamianFrizzi no problems :) The non-JS solution is not always available so I let my response here in case of someone else needs it :) \$\endgroup\$ – pomeh Jul 18 '14 at 8:35

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.