The Secret to the Page Flip in HTML5/Canvas for Windows8 and iOS

source: PageFlip.js


A few years back I wrote a tutorial called “The Secret Behind the Page Flip Technique” for Silverlight Developers while working as Creative Director at the experience agency, cynergy.  That blog post isn’t available anymore, and I haven’t touched Silverlight in a while, but even now, I still get several requests for the solution.

As I’ve been on-ramping my skills with HTML5, I decided to kill two birds with one stone and solve from scratch the advanced Page Flip Technique with Canvas. While underlying math is very similar, drawing, rotation and clipping are very different between Canvas in HTML5 and Silverlight, so I had to work out quite a few new tricks, highlighted below.


Handling Touch and Constraints

source: study1.js

The page flip above and all the interactive math illustrations work with the mouse, but also work with touch events from an iPhone or iPad and with the msPointer events for Internet Explorer 10 and the Windows 8 Consumer Preview. In order to get this to work, I found some great articles online, most notably Handling Multi-Touch and mouse input in all browsers from the IEBlog. I pulled out what I needed for the platforms I was targeting and wrapped the main functions in a file called PointerFusion.js, which you will find in the code behind. You call PointerFusion.js by passing in a DIV that will house your Canvas element, along with the down, move and up functions you want linked to it like so:


var targets;
if (document.querySelectorAll)
{
targets = document.querySelectorAll(".DivStudy1");
if ( targets.length > 0 ) {
PointerFusion(targets[0], onMouseDown, onMouseMove, onMouseUp);
} else {
return;
}
}

If you look at study1.js, you’ll see that the init() function kicks everything off, and after sending the DIV target to PointerFusion to enable mouse/pointer/touch interactions, sets up the Canvas elements and wires up a resize listener to call SetSizes() so the DIV and Canvas elements can support a responsive layout ( like this blog ) and will continue to work on a smaller iPhone / Windows Phone 7 screen up to an iPad / Microsoft Tablet or full Windows 8 / IE10 browser.

The “math” part of the page flip is very straightforward for this first step, and is all handled in the function renderMath(). Mainly, we setup a point that represents the mouse (or touch, or pointer, etc.) shown above as [M] and a generic follow point [F] that provides some graceful easing. Our red node represents the corner of the page [C] and will always try and align itself to [F]. However, since we are mapping a physical page flip, the corner of the page [C] needs to have two constraints. You can’t turn the page ‘upward’ more than the page width allows without tearing the page, so from Spine Bottom [SB] to the Edge Bottom [EB], we create the first radius constraint [R1]. A physical page being turned also can’t be turned ‘downward’ more than the page diagonal without ripping, so by taking the diagonal from the Spine Top [ST] to [EB], we create the second radius constraint [R2].

So as we finish step1.js, we have a Canvas that dynamically resizes itself to it’s DIV container for a responsive layout, a DIV that is wired for mouse, touch and pointers and renders acceptably in iPhone, iPad, Windows Phone 7, touch optimized Windows 8 experiences, and the common mouse. Dragging your finger moves the corner [C], but only within the constraints defined by [R1] and [R2]. Not a bad start, there is is still a lot more.

Calculating the Critical Triangle

source: study2.js

In order for the page flip technique to work, we need to calculate a critical triangle that gives us the angle of the page being flipped, as well as the angle of the clipping mask. This triangle is formed by finding the bisector [T0] of the corner [C] and the edge bottom [EB]. Once we have [T0], we shoot out a line perpendicular to the bisector toward the page bottom [T1]. You then close up the triangle with [T2].

Here is what the math looks like:


bisector.x = corner.x + .5*(edgeBottom.x - corner.x);
bisector.y = corner.y + .5*(edgeBottom.y - corner.y);
bisectorAngle = Math.atan2(edgeBottom.y - bisector.y, edgeBottom.x - bisector.x);
bisectorTangent = bisector.x - Math.tan(bisectorAngle) * (edgeBottom.y - bisector.y);
if ( bisectorTangent < 0 ) bisectorTangent = 0;
tangentBottom.x = bisectorTangent;
tangentBottom.y = edgeBottom.y;
bisectorBottom.x = bisector.x;
bisectorBottom.y = tangentBottom.y;

The Page and the Clipping Region

source: study3.js

Once we have the critical triangle, we have the angles required to calculate the position and rotation of both the Page sheet and what will become our Clipping Region. If you look at the code for Study3.js, you'll see that the work is done in a function called drawSheets(). The two angles we care about are the pageAngle and the clipAngle. They are calculated like so:


var clipAngle = Math.atan2(tangentBottom.y - bisector.y, tangentBottom.x - bisector.x);
if ( clipAngle < 0 ) clipAngle += Math.PI;
clipAngle -= Math.PI/2;

Placing and rotating the Page canvas (shown in red) is relatively straight forward. The place it at [C] and rotate it by the calculated pageAngle. The code looks like this:


clipctx.save();
clipctx.translate(corner.x, corner.y);
clipctx.rotate(pageAngle);
clipctx.drawImage(sheetctx.canvas,0,-pageHeight);
clipctx.restore();

Calculating the clipping region is a bit harder. We need to make sure the clipping region is fixed to [T1], but rotated by clipAngle. To do this, I wrote a helper function that takes in a point and an angle of rotation and returns that value rotated around [T1]. The function looks like this:


function rotateClipPoint(_p, angle) {
var result = new Point();
_p.x -= tangentBottom.x;
_p.y -= tangentBottom.y;
result.x = (_p.x * Math.cos(angle)) - (_p.y*Math.sin(angle));
result.y = Math.sin(angle)*_p.x + Math.cos(angle)*_p.y;
result.x += tangentBottom.x;
result.y += tangentBottom.y;
return result;
}

Once we have those clipping points, for Study3 we use them define a filled path that represents the clipping region we will use later (shown in blue). In the final step, we will use these calculated points to define an actual clipping path for a canvas that wraps the page canvas being rendered.

Implementing the Clip

source: study4.js

The biggest challenge in implementing the Page Flip technique in Canvas isn't the math, but figuring out the clipping. The problem I ran into was that drawing content from a canvas into another canvas first in retained mode and then applying a clip function wouldn't work. However, if I defined the clip first and then attempted to save() restore() on the drawing context to draw a rotated element, restore() call would also wipe out the clip! If you don't understand what that means, just play with it enough and trust me, you will. It was infuriating.

The way I was able to work around it was two fold. I needed to force a full drawing context reset by setting the width property on the parent canvas and rotate, nest, render and clip from scratch on every loop. There are three canvas and drawing context we are using in Study 4. SheetCTX is the canvas drawing context we are using for our page placeholder ( if you look at the final code for PageFlip.js you will see that SheetCTX is not only responsible for the page being flipped, but also the dynamic shadow of the page curl as well ). SheetCTX is rendered into the drawing context of the wrapper canvas that handles the clipping, called ClipCTX. Once the position and angle of the page is rendered into SheetCTX, ClipCTX uses the clipping points to define a Clipping path region and render SheetCTX into itself. At this point, we render SheetCTX into our main drawing context, CTX to interact with the rest of the elements on the screen. The full function looks like so:


function drawSheets()
{
var pageAngle = Math.atan2(tangentBottom.y - corner.y, tangentBottom.x - corner.x);
var clipAngle = Math.atan2(tangentBottom.y - bisector.y, tangentBottom.x - bisector.x);
if ( clipAngle < 0 ) clipAngle += Math.PI;
clipAngle -= Math.PI/2;
sheetCanvas.width = pageWidth;
sheetctx.fillStyle = "rgba(255,0,0,.3)";
sheetctx.fillRect(0,0,pageWidth, pageHeight);
// CALCULATE THE CLIPPING CORNERS
clipPoint0 = rotateClipPoint(new Point(tangentBottom.x, tangentBottom.y+50), clipAngle);
clipPoint1 = rotateClipPoint(new Point(tangentBottom.x-pageWidth, tangentBottom.y+50), clipAngle);
clipPoint2 = rotateClipPoint(new Point(tangentBottom.x-pageWidth, tangentBottom.y-550), clipAngle);
clipPoint3 = rotateClipPoint(new Point(tangentBottom.x, tangentBottom.y-550), clipAngle);
// RESET THE CLIPCANVAS AND CREATE CLIPPING REGION
clipCanvas.width = WIDTH;
clipctx.beginPath();
clipctx.moveTo(clipPoint0.x, clipPoint0.y);
clipctx.lineTo(clipPoint1.x, clipPoint1.y);
clipctx.lineTo(clipPoint2.x, clipPoint2.y);
clipctx.lineTo(clipPoint3.x, clipPoint3.y);
clipctx.closePath();
clipctx.clip();
// DRAW THE UPDATED PAGE BEING TURNED
clipctx.translate(corner.x, corner.y);
clipctx.rotate(pageAngle);
clipctx.drawImage(sheetctx.canvas,0,-pageHeight);
// DRAW THE CORNER
ctx.drawImage(clipctx.canvas,0,0);
}

Conclusion

The final PageFlip experience shown at the top of this post is built directly on top of Study4, but uses graphics for the Pages being flipped and some well placed (and calculated) shadow PNGs to help sell the illusion. Creating a professional level Page Flip experience in Canvas was a great learning experience for myself and hopefully the code and techniques above will help you ramp up your own skills. For sure there are optimizations that can be done and the technique shown only handles flipping "from the bottom", but the solution should be scalable to fit your needs.

There is a lot of potential with HTML5 and Canvas, even in the face of a more "responsive layout" web and multiple interaction metaphors across different devices. If you create something awesome with any of this, please put a link in the comments. I'd love to check it out!


@rickbarraza


33 comments

  1. KRK

    That is AWESOME !!!

    Will have to read it again and again to wrap my head around it.

    Thanks for trying to push through the idea of Page Flip for HTML 5 Fans.

    KRK

  2. Mr. Entropy

    Simply amazing. I’ll need to read and re-read this to truly absorb it. On a very minor note, the final link to PageFlip.js doesn’t appear to be valid? Thanks for writing this!

  3. Pingback: HTML 5 | Pearltrees
  4. Pingback: The Secret to the Page Flip in HTML5/Canvas for Windows8 and iOS | Kevin Leetham's Techno Babble
  5. Rick Barraza

    @Dave t – Thanks! Yeah, I was using an iPhone up until two days ago (bought a Nokia Lumia 900 finally ;) and just noticed that today as well. I’m looking into and asking around, but the issue lies in IE9 on Windows Phone overriding any touch inputs as a resizing gesture. If you tap, it fires only 1 mouseMove event after around 2 seconds but then freezes again. It’s a massive pain. I also went back to the original article that I based PointerFusion.js on and sure enough, it is broken on Windows Phone 7 there as well. Very frustrating. If I figure it out, I’ll definitely post the solution here or at Coding4Fun and let you know.

    rk

  6. Ja

    I haven’t yet delved into the world of canvas too heavily… However html5 and all of its implementations excite me. This is an awesome article which solves a lot of the issues I was having trying to implement the same technique but on the DOM. I had one question though. If I wanted to, once the page had reached is resting place after a flip, repeat the flipping action to make a book of say 30 pgs… How would I be able to do so? From what I understand about canvas, all of its objects are just functions in the context, so how would I go about applying text to one or referring to an obj in the canvas in another function in my script? Again, your awesome man for doing this tutorial.

    • Rick Barraza

      @Ja – No problem. What you’re asking for ( how to get it to work with multiple pages ) is what I show at the very top. Just download the PageFlip.js file ( and PointerFunction.js ) or do a F12 in the browser and look for those 2 files. It will show you how to load up multiple pages and swap them out in the effect. What this tutorial doesn’t show, though, is inverting the math to handle flipping a page back ( or from the top corners ), but I’ll leave that as an exercise for anyone who wants to take this farther. Glad you liked it. Check out the PageFlip.js file for what you’re looking for.

      rk

  7. Jesse

    Very well done, Canvas is pretty darn awesome. Can’t wait until all browsers are GPU optimized and able to handle these types of experiences!

  8. Ryan Keeter

    You Have Earned: Epic Post Badge.

    The way you broke down this complex subject says two things: 1) you are far smarter than I, and 2) your ability to extrapolate overarching and accessible thoughts from intricate code is a gift.

    • Rick Barraza

      @Ryan – LOL, not sure about that. Great site, btw! I’m so new to all this knockout, responsive web, node.js, etc. stuff sites like yours are a great help. Now I know who to hit up when I have to start doing knockout stuff! Keep up the great work.

  9. Mike Wolf

    Pertaining touch events and useable mouse move :) in ie9 on wp7.5 sadly no real work around in browser is possible to my knowledge. Sadly the touch events simply aren’t present, and mouse move events get eaten by the assumption of a gesture. You can see this best when attempting to access something like maps in browser or cut the rope on ie 9 on wp7

    • Rick Barraza

      @Mike – Thanks Mike. That’s exactly what I’ve been finding too, unfortunately. And I know you and your team of wizards have spent a lot of time checking it out. Thanks for the update. If I find anything out around here in Redmond, I’ll ping it out when I can.

  10. Pingback: The Secret to the Page Flip in HTML5/Canvas for Windows8 and iOS | rbarraza.com | Web tools and technologies | Scoop.it
  11. alessio

    excellent explanation!
    Just wondering how much difficult would it be to implement a nested flipping pages, I mean.. you have this book and this flipping page script. Somewhere in some of its page you find kind of post-It that can be flipped in the same way.. of course, the post-it can be sticked at any angle :-)

  12. Sreeram

    Hey Rick,
    Could you give some pointers on how to make the page flip from the bottom left page also, as it flips from the bottom right at the moment.

  13. Daniel

    I get the general idea of it all, but I got to be honest with you – it’s so much to take in that I get overwhelmed when I look at the details of it all. I want to expand on this…but this beast scares me. If I took a week to work on this then fine, but that’s a lot of time for me to commit to a page turn (sexy as it may be) I guess I would have appreciated an even more basic aproach to it.

    Instead of staring out with the really advanced x/y coords responsive ready layout for the div and canvas maybe it would have been better to simply start out with explaining how you are going to animate it, that you are using 3 canvases and how they will work.

    Maybe then go through the canvases one by one, move on to the math bit, and make a few incremental examples here with maybe start off with just animating one of them to begin with using a simple fold in the corner that you can click and then incrementally add the other canvases and then finally add mouse movement.

    Going through the code there are a few things that I just don’t get as well, like the drawCircle function (amongst others) I can’t see that it’s used, and when I’m allready a bit edgy about how advanced this all is having extra, dead code in there makes it look even more advanced and scary.

    I guess I’m stupid, but a dumbed down version would have worked better for me.

  14. Pingback: 2012 Archive of Hanselman's Newsletter of Wonderful Things - Scott Hanselman
  15. Brian

    I am trying to reproduce this with a page flip up instead of right to left. Any suggestions on getting this to work in landscape?

  16. Monero

    That’s an excellent work and the best page flip implementation i have seen with javascript yet. The critical triangle and all the math used in this is terrific.
    Thanks Rick!

Post a comment

You may use the following HTML:
<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>