Friday 23 December 2022

Web Tutorial: The Christmas Flipbook (Part 4/4)

The book works now. But it's kind of boring. We need to animate this shit!

I'm going to show you how to make the pages move as if you're physically flipping the page. First of all, we need to look at 3D transforms. Add this code to renderPage(). This adds the rotatable CSS class to the div if the placeholder is left_front or right_front. That's because only those will be "flipping".
renderPage: function(id, obj)
{
    var div = $("<div></div>");
    div.addClass(obj.class);

    if (id == "left_front" || id == "right_front")
    {
        div.addClass("rotatable");
    }


    var pagenumber = $("<div></div>");


For rotatable, left and right sides have slightly different properties. For a page on the left, you will 3D rotate it using the right edge as the point of origin, and vice versa for pages on the right side. Therefore, we use the transform-origin property.
.page
{
    width: 196px;
    height: 296px;    
    padding: 50px;
    margin-top: 2px;            
}

#left_front .rotatable
{
    -webkit-transform-origin: 100% 50%;
    transform-origin: 100% 50%;
    transform: rotateY(0deg);
}

#right_front .rotatable
{
    -webkit-transform-origin: 0% 50%;
    transform-origin: 0% 50%;
    transform: rotateY(0deg);
}


#left .page
{

    background: -moz-linear-gradient(left,  rgb(200, 200, 150) 0%, rgb(200, 200, 150) 25%, rgb(150, 150, 50) 70%, rgb(50, 50, 10) 100%);
    background: -webkit-linear-gradient(left,  rgb(200, 200, 150) 0%, rgb(200, 200, 150) 25%, rgb(150, 150, 50) 70%, rgb(50, 50, 10) 100%);
    background: linear-gradient(to right,  rgb(200, 200, 150) 0%, rgb(200, 200, 150) 25%, rgb(150, 150, 50) 70%, rgb(50, 50, 10) 100%);
    float: right;
}


Now, at flip(), add this. This makes sure that page in right_front rotates when "flipped". Also remove the call to renderView() for now. We will reintroduce it later.
flip: function(dir)
{
    if (this.currentView + dir < 0) return;
    if (this.currentView + dir > this.views.length -1) return;

    this.currentView = this.currentView + dir;

    //this.renderView();

    if (dir > 0)
    {
        $("#right_front .rotatable").attr("style", "transform: rotateY(-85deg); transition-duration: 1s");           }

}    


Test this by clicking the right button. You'll see the cover "flip" in 1 second. And you can now see the underlying page in right_back. Notice the final angle of the cover? Far from perfect. But we can fix this with some perspective!






No, I don't mean questioning the meaning of life. I mean in the CSS. The parent divs for the page, namely left_front and right_front, need to have perspective applied. I find 2000 pixels work for me, but you could try adjusting till you hit your magic number. The lower the number, though, the greater the distortion.
.page
{
    width: 196px;
    height: 296px;    
    padding: 50px;
    margin-top: 2px;            
}

#left_front, #right_front
{
    -webkit-perspective: 2000px;
    perspective: 2000px;
}


#left_front .rotatable
{
    -webkit-transform-origin: 100% 50%;
    transform-origin: 100% 50%;
    transform: rotateY(0deg);
}

#right_front .rotatable
{
    -webkit-transform-origin: 0% 50%;
    transform-origin: 0% 50%;
    transform: rotateY(0deg);
}


Try again. The final angle of the "flipped" cover looks better now. See what else is wrong? The next or previous pages are not shown, so this doesn't feel like a real book with depth.




That's where left_middle and right_middle come in. In renderView(), we populate these divs with appropriate content if the views are after the first page or before the last page.
renderView: function()
{
    $(".pageholder").html("");

    if (this.currentView > 0)
    {
        if (this.currentView > 2)
        {
            this.renderPage("left_back", this.views[1].left);
        }

        if (this.currentView > 1)
        {
            this.renderPage("left_middle", this.views[this.currentView - 1].left);
        }


        this.renderPage("left_front", this.views[this.currentView].left);
    }

    if (this.currentView < this.views.length - 1)
    {
        if (this.currentView < this.views.length - 3)
        {
            this.renderPage("right_back", this.views[this.views.length - 2].right);
        }

        if (this.currentView < this.views.length - 2)
        {
            this.renderPage("right_middle", this.views[this.currentView + 1].right);
        }


        this.renderPage("right_front", this.views[this.currentView].right);
    }
},


Nice!






OK, now for more animation. We need a setTimeout() function. Remember the duration of the animation has been set for 1 second, or 1000 milliseconds. So the timeout will be set at slightly less than that, maybe 990 milliseconds.
//this.renderView();

if (dir > 0)
{
    $("#right_front .rotatable").attr("style", "transform: rotateY(-85deg); transition-duration: 1s");    

    setTimeout(
        ()=>
        {

        },
        990
    )  
             
}


Remember we disabled that call to renderView() earlier? Well, we will move it here now, which means renderView() only gets called when the right side page is done "flipping".
setTimeout(
    ()=>
    {
        this.renderView();
    },
    990
)


And at this point, we set the left side page to rotate, but with the transition-duration property set at 0 so that it is instantaneous. We want to make it look like a continuation of that right side page "flipping" to the left, even though it's now a totally different div! If you're wondering what happened to the previously "flipped" right side page, the renderView() call took care of it by resetting the display.
setTimeout(
    ()=>
    {
        this.renderView();
        $("#left_front .rotatable").attr("style", "transform: rotateY(85deg); transition-duration: 0s");
    },
    990
)


There, see? After "flipping", the view is reset using renderView(), but with the left side page rotated.




What we want to do is complete the "flip". We use another setTimeout() call, this time just after that last statement. 10 milliseconds should do it.
setTimeout(
    ()=>
    {
        this.renderView();
        $("#left_front .rotatable").attr("style", "transform: rotateY(85deg); transition-duration: 0s");
        setTimeout(
            ()=>
            {

            },
            10
        );

    },
    990
)    


And then set transition-duration back to 1 second, and complete the rotation to 0 degrees.
setTimeout(
    ()=>
    {
        this.renderView();
        $("#left_front .rotatable").attr("style", "transform: rotateY(85deg); transition-duration: 0s");
        setTimeout(
            ()=>
            {
                $("#left_front .rotatable").attr("style", "transform: rotateY(0deg); transition-duration: 1s");  
            },
            10
        );
    },
    990
)


Go on, try flipping! To avoid repeating myself, let's do the same for the opposite direction, but reversed.
if (dir > 0)
{
    $("#right_front .rotatable").attr("style", "transform: rotateY(-85deg); transition-duration: 1s");    

    setTimeout(
        ()=>
        {
            this.renderView();
            $("#left_front .rotatable").attr("style", "transform: rotateY(85deg); transition-duration: 0s");
            setTimeout(
                ()=>
                {
                    $("#left_front .rotatable").attr("style", "transform: rotateY(0deg); transition-duration: 1s");        
                },
                10
            );
        },
        990
    )                
}

if (dir < 0)
{
    $("#left_front .rotatable").attr("style", "transform: rotateY(85deg); transition-duration: 1s");    

    setTimeout(
        ()=>
        {
            this.renderView();
            $("#right_front .rotatable").attr("style", "transform: rotateY(-85deg); transition-duration: 0s");
            setTimeout(
                ()=>
                {
                    $("#right_front .rotatable").attr("style", "transform: rotateY(0deg); transition-duration: 1s");                                },
                10
            );
        },
        990
    )                
}


At certain points, I added code to disable the buttons while the page is "flipping". Not absolutely necessary, but it prevents a case where the user keeps repeatedly clicking on the buttons before the animation has had a chance to complete and causing the animation to behave awkwardly.
if (dir > 0)
{
    $("button").prop("disabled", true);
    $("#right_front .rotatable").attr("style", "transform: rotateY(-85deg); transition-duration: 1s");    

    setTimeout(
        ()=>
        {
            this.renderView();
            $("#left_front .rotatable").attr("style", "transform: rotateY(85deg); transition-duration: 0s");
            setTimeout(
                ()=>
                {
                    $("#left_front .rotatable").attr("style", "transform: rotateY(0deg); transition-duration: 1s");    

                    setTimeout(
                        ()=>
                        {
                            $("button").prop("disabled", false);    
                        },
                        1000
                    );    

                },
                10
            );
        },
        990
    )                
}

if (dir < 0)
{
    $("button").prop("disabled", true);
    $("#left_front .rotatable").attr("style", "transform: rotateY(85deg); transition-duration: 1s");    

    setTimeout(
        ()=>
        {
            this.renderView();
            $("#right_front .rotatable").attr("style", "transform: rotateY(-85deg); transition-duration: 0s");
            setTimeout(
                ()=>
                {
                    $("#right_front .rotatable").attr("style", "transform: rotateY(0deg); transition-duration: 1s");    

                    setTimeout(
                        ()=>
                        {
                            $("button").prop("disabled", false);    
                        },
                        1000
                    );  
                                     
                },
                10
            );
        },
        990
    )                
}


See what I mean? The buttons disable while flipping!




One last thing we need to do is remove the blue outline.
.pageholder
{
    width: 100%;
    height: 100%;
    outline: 0px solid blue;
    float: left;
}


And here's your flipbook!




Merry Christmas, readers!

This was fun. A bit long, but totally worth it.

Don't flip out!
T___T

No comments:

Post a Comment