Flip a sprite in canvas

16,999

Solution 1

You can transform the canvas drawing context without flipping the entire canvas.

c.save();
c.scale(-1, 1);

will mirror the context. Draw your image, then

c.restore();

and you can draw normally again. For more information, see https://developer.mozilla.org/en-US/docs/Web/Guide/HTML/Canvas_tutorial/Transformations

Solution 2

While Gaurav has shown the technical way to flip a sprite in canvas...

Do not do this for your game.

Instead make a second image (or make your current image larger) that is a flipped version of the spite-sheet. The performance difference between "flipping the context and drawing an image" vs "just drawing an image" can be massive. In Opera and Safari flipping the canvas results in drawing that is ten times slower, and in Chrome it is twice as slow. See this jsPerf for an example.

Pre-computing your flipped sprites will always be faster, and in games canvas performance really matters.

Share:
16,999

Related videos on Youtube

James Dawson
Author by

James Dawson

Updated on July 17, 2022

Comments

  • James Dawson
    James Dawson almost 2 years

    I'm using canvas to display some sprites, and I need to flip one horizontally (so it faces left or right). I can't see any method to do that with drawImage, however.

    Here's my relevant code:

    this.idleSprite = new Image();
    this.idleSprite.src = "/game/images/idleSprite.png";
    this.idleSprite.frameWidth = 28;
    this.idleSprite.frameHeight = 40;
    this.idleSprite.frames = 12;
    this.idleSprite.frameCount = 0;
    
    this.draw = function() {
            if(this.state == "idle") {
                c.drawImage(this.idleSprite, this.idleSprite.frameWidth * this.idleSprite.frameCount, 0, this.idleSprite.frameWidth, this.idleSprite.frameHeight, this.xpos, this.ypos, this.idleSprite.frameWidth, this.idleSprite.frameHeight);
                if(this.idleSprite.frameCount < this.idleSprite.frames - 1) { this.idleSprite.frameCount++; } else { this.idleSprite.frameCount = 0; }
            } else if(this.state == "running") {
                c.drawImage(this.runningSprite, this.runningSprite.frameWidth * this.runningSprite.frameCount, 0, this.runningSprite.frameWidth, this.runningSprite.frameHeight, this.xpos, this.ypos, this.runningSprite.frameWidth, this.runningSprite.frameHeight);
                if(this.runningSprite.frameCount < this.runningSprite.frames - 1) { this.runningSprite.frameCount++; } else { this.runningSprite.frameCount = 0; }
            }
        }
    

    As you can see, I'm using the drawImage method to draw my sprites to the canvas. The only way to flip a sprite that I can see is to flip/rotate the entire canvas, which isn't what I want to do.

    Is there a way to do that? Or will I need to make a new sprite facing the other way and use that?

  • James Dawson
    James Dawson over 12 years
    Thanks. What I ended up doing was adding another sprite to my existing one (below it) with my character running the other way, and added some code to use that set of sprites depending on the direction. Like this: i.imgur.com/uIPvF.png
  • James Dawson
    James Dawson over 12 years
    Thank you! But due to Simon's answer, I'll be using his technique. I'll mark yours as the accepted answer though as it is answering my question :)
  • Simon Sarris
    Simon Sarris over 12 years
    Looks good! Though I would rearrange the new row: line up the flipped version of each vertically so the only thing you have to change when flipping is the y coordinate.
  • Jeff Gates
    Jeff Gates almost 12 years
    Gaurav's answer is really only performance costly due to the ctx.save/restore. In fact if you simply call ctx.scale(...) every time, hilariously the flipped version draws faster: jsperf.com/ctx-flip-performance/3. (it's even faster if you call ctx.scale(-1) for the flipped and nothing for a the non-flipped case: jsperf.com/ctx-flip-performance/4).
  • Simon Sarris
    Simon Sarris almost 12 years
    Jeff you raise a good point that save and restore aren't necessary, but your result was a fluke, see the results now. You may have begun the test before the browser finished loading the page or Java (used in the timer). I'd argue that the test isn't accurate at /4 because the work required to flip also must include the work to return the transformation back to its normal state. This means at a minimum a second call to ctx.scale(-1,1). Here's a "fair" test, and flipping is slower in every browser and mobile device tested, sometimes very much so: jsperf.com/ctx-flip-performance/5
  • Jeff Gates
    Jeff Gates almost 12 years
    I had thought .scale() set the transforms scale as opposed to multiplying it. The multiply is what is implied in the w3.org docs (dev.w3.org/html5/2dcontext/#dom-context-2d-scale), which would indeed invalidate test /4. (It uses the term 'add' which is a bit ambiguous). However, that's not what it seems to be doing, at least on chrome: jsperf.com/ctx-flip-performance/8.
  • Simon Sarris
    Simon Sarris almost 12 years
    Woah! You just showed something really important. Jsperf's setup function is broken and not clearing the canvas! See jsperf.com/ctx-flip-performance/10 for what happens if I explicitly clear inside of the test, and and see jsfiddle.net/sX2jT for what really happens. scale always multiplies, its a bug with jsperf we are seeing :(
  • Scott
    Scott almost 9 years
    Unfortunately this does not support PNGs :(