Friday 23 December 2016

Web Tutorial: Scroll-down Christmas Carol

Christmas Day is looming!

For that, I traditionally write a web tutorial to commemorate the occasion. This year, it will be sinfully simple - a scroll-down card. Y'know, one of those things where content changes on the screen when you scroll.

In order to do that, however, we'll need to start off with a standard HTML layout.
<!DOCTYPE html>
<html>
    <head>
        <title>Xmas Carol</title>

        <style>

        </style>

        <script>

        </script>
    </head>

    <body>

    </body>
</html>


Because we'll be screwing around with div layouts, let's add this to the CSS. It turns all divs a translucent green.
        <style>
            div{background-color:rgba(0,255,0,0.2);}
        </style>


Add a div to the body. The id will be container.
    <body>
        <div id="container">

        </div>
    </body>


This is the style for container. Yep, you didn't read it wrong. The width is set to 100%, and the height is 10 times the height of the screen. Which puts it at a whopping 1000%. This forces the page to be scrollable. However, if you run the code now, you might not see a damn thing even though ideally, you're supposed to see a translucent green overlay.
        <style>
            div{background-color:rgba(0,255,0,0.2);}

            #container
            {
                width:100%;
                height:1000%;
            }
        </style>


You may not see anything until you do this. This ensures that the html and body elements are set at 100% of screen height. This is supposed to be the default, but you know how it is with different browsers...
        <style>
            div{background-color:rgba(0,255,0,0.2);}

            html, body
            {
                height:100%;
                padding:0px;
            }

            #container
            {
                width:100%;
                height:1000%;
            }
        </style>


Now here you go. This is what you should see. padding:0px ensures that there are no ugly spaces at the top.

OK, now let's add another div, id view_wrapper.
    <body>
        <div id="container">
            <div id="view_wrapper">

            </div>
        </div>
    </body>


We style it like so. Width is set to full, height is at 80%, and there's a 5% margin at the top. Most importantly, the position property has been set to fixed so it will remain in the middle of your screen no matter where you scroll.
        <style>
            div{background-color:rgba(0,255,0,0.2);}

            html, body
            {
                height:100%;
                padding:0px;
            }

            #container
            {
                width:100%;
                height:1000%;
            }

            #view_wrapper
            {
                width:100%;
                height:80%;
                position:fixed;
                margin-top:5%;
            }
        </style>


Try it!

One more div nested. Set the id to view.
    <body>
        <div id="container">
            <div id="view_wrapper">
                <div id="view">

                </div>
            </div>
        </div>
    </body>


Style it like this. It occupies 80% of view_wrapper's width, all of its height, and the margin property is set to center it. Properties such as text-align, font-size, font-family and font-weight are optional. Do what you want with those - unless you get really extreme, it won't change the functionality much.
        <style>
            div{background-color:rgba(0,255,0,0.2);}

            html, body
            {
                height:100%;
                padding:0px;
            }

            #container
            {
                width:100%;
                height:1000%;
            }

            #view_wrapper
            {
                width:100%;
                height:80%;
                position:fixed;
                margin-top:5%;
            }

            #view
            {
                width:80%;
                height:100%;
                margin: 0 auto 0 auto;
                text-align:center;
                font-size:5em;
                font-family:verdana;
                font-weight:bold;
            }
        </style>


That's what it should look like now. The view div is meant to contain the content of your text!

Now we're going to write some JavaScript to populate the view div. We start off by declaring a variable lyrics, and populate it with lines from a standard Christmas song. Note that each line is denoted with a "|" symbol.
        <script>
            var lyrics="We wish you a Merry Christmas|We wish you a Merry Christmas|We wish you a Merry Christmas|And a Happy New Year!";
        </script>


Next, we create an array named lines, and set it to contain each line of the lyrics, using the "|" symbol.
        <script>
            var lyrics="We wish you a Merry Christmas|We wish you a Merry Christmas|We wish you a Merry Christmas|And a Happy New Year!";

            var lines=lyrics.split("|");
        </script>


Next, we write a function getScrollPercentage(). It will return the percentage of how much you've scrolled from the top. window.innerHeight has to be multiplied by 10 in this case because you're measuring it against the entire scrollable content, which is 10 times your screen height. The height of container was set at 1000% percent, remember?

So when you reach the bottom, it should be 100%, right? Wrong! And I'll prove it.
        <script>
            var lyrics="We wish you a Merry Christmas|We wish you a Merry Christmas|We wish you a Merry Christmas|And a Happy New Year!";

            var lines=lyrics.split("|");

            function getScrollPercentage()
            {
                return (document.documentElement.scrollTop/(window.innerHeight*10))*100;
            }
        </script>


Add this to your code, and run it.
    <body onscroll="console.log(getScrollPercentage())">


Check your console (Ctrl+Shift+i should do it) and you'll see when you scroll all the way to the bottom, the readout is 90% at most.

So add this line. This is to help decide what percentage of scrollable content to be dedicated to each line, ie. each item in the lines array. Instead of 100%, we only have 90% to work with, so it's (90/lines.length). Since we want to err on the side of caution, we'll round it down using Math.floor().
            var lines=lyrics.split("|");

            var segment_per_line=Math.floor(90/lines.length);

            function getScrollPercentage()
            {
                return (document.documentElement.scrollTop/(window.innerHeight*10))*100;
            }


Now we write the function scrolling(). We set a variable, scrolled, to the current scrolled percentage defined in getScrollPercentage(). Then we get the line number, another variable called line_no, by dividing scrolled with segment_per_line. Erring on the side of caution, again, we use Math.floor() to round it down. Then we set the innerHTML property of the view div to the appropriate item of the lines array.
            function getScrollPercentage()
            {
                return (document.documentElement.scrollTop/(window.innerHeight*10))*100;
            }

            function scrolling()
            {
                var scrolled=getScrollPercentage();
                var line_no=Math.floor(scrolled/segment_per_line);

                document.getElementById("view").innerHTML=lines[line_no];
            }


Change this line, and run your code.
    <body onscroll="scrolling()">


So you should get different lines at different scroll heights! Try scrolling all the way down from the top.

The problem with this...

...is that it's not terribly friendly. You scroll, an entire line appears, then as you scroll, it's replaced by a whole new line just like that. In the case of our chosen stanza, the first three lines are identical! How you you even tell when one line changes to the next?!

The answer: Make each word appear individually, by line.

For that to happen, add this to your code. It runs through each item in the lines array and turns it into an array too. So instead of an array of lines, you have an array of arrays, and each of these sub arrays will contain the words to the lines, in sequence!
            var lines=lyrics.split("|");

            var segment_per_line=Math.floor(90/lines.length);

            for (var i=0;i<lines.length;i++)
            {
                lines[i]=lines[i].split(" ");
            }

            function getScrollPercentage()
            {
                return (document.documentElement.scrollTop/(window.innerHeight*10))*100;
            }


In other words, instead of this...
lines[0] "We wish you a Merry Christmas"
lines[1] "We wish you a Merry Christmas"
lines[2] "We wish you a Merry Christmas"
lines[3] "And a Happy New Year!"

...you get this!
lines[0] ["We","wish","you","a","Merry","Christmas"]
lines[1] ["We","wish","you","a","Merry","Christmas"]
lines[2] ["We","wish","you","a","Merry","Christmas"]
lines[3] ["And","a","Happy","New","Year!"]

Now modify your scrolling() function. The variable segment_completion gives you how many total segments has been completed already. So, for example, if you're at line number 3 and each line is 5% of scroll completion, you have scrolled at least 15%! The next line would be at 20%, so how many words of the current line you display will be the progress you've made from 15% to 20%.
            function scrolling()
            {
                var scrolled=getScrollPercentage();
                var line_no=Math.floor(scrolled/segment_per_line);
                var segment_completion=line_no*segment_per_line;

                document.getElementById("view").innerHTML=lines[line_no];
            }


For that, we'll create a few function, getWordsFromLine(). We'll use this function to determine how many words from the current line (which is now an array, remember?) to display, and display those words instead of the entire line. For that, we pass in the entire sub-array from the lines array which will be defined as lines[line_no], and a value which gives you the percentage scrolled since the last completed segment.
            function getWordsFromLine(line,completion)
            {

            }

            function scrolling()
            {
                var scrolled=getScrollPercentage();
                var line_no=Math.floor(scrolled/segment_per_line);
                var segment_completion=line_no*segment_per_line;

                var words=getWordsFromLine(lines[line_no], scrolled-segment_completion));
                document.getElementById("view").innerHTML=words;
            }


Here, we calculate the completion percentage of the current segment, and assign it to the completion_ratio variable. Then we use this ratio to determine how many words out of the entire line will be displayed, using the number of words (line.length) and completion_ratio.
            function getWordsFromLine(line,completion)
            {
                var completion_ratio=(completion/segment_per_line);
                var words_displayed=Math.ceil(line.length*completion_ratio);
                var words="";
            }


After that, we iterate through the line array using a For loop, adding the necessary number of words, and return the string generated.
            function getWordsFromLine(line,completion)
            {
                var completion_ratio=(completion/segment_per_line);
                var words_displayed=Math.ceil(line.length*completion_ratio);
                var words="";

                for (var i=0;i<words_displayed;i++)
                {
                    words+=line[i]+" ";
                }

                return words;
            }


Run your code! What happens? Bet it worked, didn't it? But you may run into a small problem near the end because lines[line_no] may be undefined due to line_no being larger than the total number of items in lines. So just do this...
            function scrolling()
            {
                var scrolled=getScrollPercentage();
                var line_no=Math.floor(scrolled/segment_per_line);
                var segment_completion=line_no*segment_per_line;

                var words=(lines[line_no]==undefined?"":getWordsFromLine(lines[line_no],scrolled-segment_completion));
                document.getElementById("view").innerHTML=words;
            }


What it basically does is check if lines[line_no] is undefined, and returns an empty string if so. If not, run the getWordsFromLine() function as per normal.

Further improvements?

Well, now your lines appear word by word, but they skip onwards to the next line without pause. That's really unnatural.

So just modify your code like so. Instead of a single "|" symbol, we use a double "||". This ensures that the lines array has more items, specifically an empty item between each line of the song.
            var lyrics="We wish you a Merry Christmas||We wish you a Merry Christmas||We wish you a Merry Christmas||And a Happy New Year!||Good tidings to you||To you and your kin||Good tidings for Christmas||And a Happy New Year!";


Run your code. Is there a nice pause between each line? But let's add an If conditional to this. As in, the content only changes if there are actually words to display. Re-run your code. Now there won't be any blank pauses between each line!
            function scrolling()
            {
                var scrolled=getScrollPercentage();
                var line_no=Math.floor(scrolled/segment_per_line);
                var segment_completion=line_no*segment_per_line;

                var words=(lines[line_no]==undefined?"":getWordsFromLine(lines[line_no],scrolled-segment_completion));

                if (words.length>0)
                {
                    document.getElementById("view").innerHTML=words;
                }
            }


So now you have a nice scrolling card. Let's change the background a bit to remove the placeholder green.
            div{background-color:rgba(0,255,0,0.0);}


Then we'll add this tile to spruce things up.

xmas2016_bg.jpg

            body
            {
                background:url(xmas2016_bg.jpg);
            }


Nice, right?!


Little extras - a mini caroller

Now, for all intents and purposes, your scrolling card is done. But here's a little something to complement the look. I've added the HTML and CSS for a little South Park type caroller. I'm not going to show you how it's done because that's totally not the point of this tutorial at all, but here's the code anyway.
            <div id="view_wrapper">
                <div id="view">

                </div>
                <div id="caroller">
                    <div class="head" id="head">
                        <div class="hair">

                        </div>
                        <div class="mouth" id="mouth">

                        </div>
                    </div>
                    <div class="body">
                        <div class="sheet">

                        </div>
                    </div>
                    <div class="feet">

                    </div>
                </div>
            </div>


Here's the CSS. Change the colors if you like.
            #view
            {
                width:80%;
                height:100%;
                margin: 0 auto 0 auto;
                text-align:center;
                font-size:5em;
                font-family:verdana;
                font-weight:bold;
            }

            #caroller
            {
                width:200px;
                height:300px;
                margin: -20% auto 0 auto;
            }

            #caroller .head
            {
                width:100px;
                height:100px;
                background-color:#AA4400;
                border-radius:50%;
                margin: 0 auto 0 auto;
            }

            #caroller .head .hair
            {
                width:100%;
                height:50%;
            }

            #caroller .head .hair::before,#caroller .head .hair::after
            {
                content:"";
                display:block;
                width:60%;
                height:100%;
                background-color:#000000;
            }

            #caroller .head .hair::before
            {
                border-radius:70% 0%;
                float:left;
                margin-left:-10%;
            }

            #caroller .head .hair::after
            {
                border-radius:0% 70%;
                float:right;
                margin-right:-10%;
            }

            #caroller .head .mouth
            {
                width:0px;
                height:0px;
                background-color:#000000;
                border-radius:50%;
                margin: 10% auto 0 auto;
            }

            #caroller .body
            {
                width:180px;
                height:150px;
                background-color:#AA0000;
                border-radius:40% 40% 0% 0%;
                margin: -5px auto 0 auto;
                padding-top:10px;
            }

            #caroller .body::before,#caroller .body::after
            {
                content:"";
                display:block;
                width:50px;
                height:50px;
                border-radius:50%;
                background-color:#FFFFFF;   
            }

            #caroller .body::before
            {
                float:left;
                margin-left:-10%;
                margin-top:50px;
            }

            #caroller .body::after
            {
                float:right;
                margin-right:-10%;
                margin-top:-30px;
            }

            #caroller .sheet
            {
                width:80%;
                height:50%;
                background-color:#FFFF00;
                margin: 0 auto 0 auto;
            }

            #caroller .feet
            {
                width:150px;
                height:50px;
                background-color:#440000;
                margin: 0 auto 0 auto;
            }

            #caroller .feet::before,#caroller .feet::after
            {
                content:"";
                display:block;
                width:60%;
                height:80%;
                border-radius:50% 50% 0% 0%;
                background-color:#220000;
                margin-top:10px;
            }

            #caroller .feet::before
            {
                float:left;
                margin-left:-10%;
            }

            #caroller .feet::after
            {
                float:right;
                margin-right:-10%;
            }


Cute little bugger, isn't he?!


Now do this to your JavaScript. This ensures that the head and mouth move in random directions when there are words to "sing", and stays still otherwise.
            function generaterandomno(varmin,varmax)
            {
                return Math.floor((Math.random() * (varmax-varmin+1)) + varmin);
            }

            function scrolling()
            {
                var scrolled=getScrollPercentage();
                var line_no=Math.floor(scrolled/segment_per_line);
                var segment_completion=line_no*segment_per_line;

                var words=(lines[line_no]==undefined?"":getWordsFromLine(lines[line_no],scrolled-segment_completion));

                if (words.length>0)
                {
                    document.getElementById("view").innerHTML=words;
                    document.getElementById("mouth").style.height=generaterandomno(10,30)+"px";
                    document.getElementById("mouth").style.width=generaterandomno(10,50)+"px";
                    document.getElementById("head").style.transform="rotate("+generaterandomno(-5,5)+"deg)";
                    document.getElementById("head").style.webkitTransform="rotate("+generaterandomno(-5,5)+"deg)";
                }
                else
                {
                    document.getElementById("mouth").style.height="0px";
                    document.getElementById("mouth").style.width="0px";   
                }
            }


Merry Christmas!

You can vary things by changing the lyrics. It should still work unless you substitute the lyrics with something long and rambling (like one of Roy Ngerng's speeches) going over 100 lines or so.

That's how I (sc)roll, baby.
T___T

No comments:

Post a Comment