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!
轉載:http://rbarraza.com/html5-canvas-pageflip/
http://world.yo2.cn/articles/page-turn-flip-algorithm.html