Showing posts with label CSS3. Show all posts
Showing posts with label CSS3. Show all posts

Monday, 8 April 2024

Spot The Bug: Tepee Transformation

Pesky little programming bugs creep on on the halpess programmer, but never fear, because Spot The Bug is back for more bug-hunting goodness!

Have at those
nasty bugs!

Today's session will be comparatively simple. I hesitate to even call it a programming bug, but it certainly has plenty to do with web development, and it is code, so let's get to it.

I was trying to render a very simple tepee logo in CSS, consisting of a V and a U, both upside down, with the V over the U, the V acting as a "roof" and the U acting as a "door". Sounds simple enough, eh? Well, if it were, we wouldn't be having this conversation.

What went wrong

This is how I started out...
<!DOCTYPE html>
<html>
    <head>
        <title>Tepee</title>

        <style>
            .icon
            {
                font-family: verdana;
                font-size: 80px;
                color: black;
                position: absolute;
                left: 0;
                top: 0;
            }
        </style>
    </head>

    <body>
        <div class="icon">V</div>
        <div class="icon">U</div>
    </body>
</html>


See this? A V and a U, crammed together in a corner.



Now I rotated both the V and the U. So they were upside down.
<!DOCTYPE html>
<html>
    <head>
        <title>Tepee</title>

        <style>
            .icon
            {
                font-family: verdana;
                font-size: 80px;
                color: black;
                position: absolute;
                left: 0;
                top: 0;
            }
        </style>
    </head>

    <body>
        <div class="icon" style="transform: rotate(180deg)">V</div>
        <div class="icon" style="transform: rotate(180deg)">U</div>
    </body>
</html>


So far so good!



I translated the U...
<!DOCTYPE html>
<html>
    <head>
        <title>Tepee</title>

        <style>
            .icon
            {
                font-family: verdana;
                font-size: 80px;
                color: black;
                position: absolute;
                left: 0;
                top: 0;
            }
        </style>
    </head>

    <body>
        <div class="icon" style="transform: rotate(180deg) ">V</div>
        <div class="icon" style="transform: translate(300px, 300px) rotate(180deg)">U</div>
    </body>
</html>


And now for the V, right?



I scaled the V, like 4 times.
<!DOCTYPE html>
<html>
    <head>
        <title>Tepee</title>

        <style>
            .icon
            {
                font-family: verdana;
                font-size: 80px;
                color: black;
                position: absolute;
                left: 0;
                top: 0;
            }
        </style>
    </head>

    <body>
        <div class="icon" style="transform: scale(4,4) rotate(180deg) ">V</div>
        <div class="icon" style="transform: translate(300px, 300px) rotate(180deg)">U</div>
    </body>
</html>


Still looking OK.



And then I translated it.
<!DOCTYPE html>
<html>
    <head>
        <title>Tepee</title>

        <style>
            .icon
            {
                font-family: verdana;
                font-size: 80px;
                color: black;
                position: absolute;
                left: 0;
                top: 0;
            }
        </style>
    </head>

    <body>
        <div class="icon" style="transform: scale(4,4) rotate(180deg) translate(300px, 200px)">V</div>
        <div class="icon" style="transform: translate(300px, 300px) rotate(180deg)">U</div>
    </body>
</html>


And boom! It disappeared!



Why it went wrong

The trick here is that I translated after rotating and scaling the V, whereas for the U, I translated before.

And why's that important?

Well, this is a mathematical thing. When I was learning Computer Graphics back in University, one of the things they taught us was that where multiple transformations are concerned, order is important. If you take scaling as multiplication (or division) and translation as addition (or subtraction), scaling before you translate will have a very different result compared to translating before scaling.

But that aside, for CSS, the transform property has a very specific sequence where multiple transformations are concerned.

How I fixed it

Simply, I got consistent and translated first, before the other transformations
<div class="icon" style="transform: translate(300px, 200px) scale(4,4) rotate(180deg)">V</div>
<div class="icon" style="transform: translate(300px, 300px) rotate(180deg)">U</div>


And now, there we had the tepee!


Well admittedly, it still needed some adjustment.
<div class="icon" style="transform: translate(300px, 220px) scale(4,4) rotate(180deg)">V</div>
<div class="icon" style="transform: translate(300px, 300px) rotate(180deg)">U</div>


But yeah, you get the idea.



Moral of the story

Even something like the order of things can throw you right off. Not everything is equivalent.

See U around!
T___T

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

Tuesday, 13 September 2022

Five Unpleasant Things About Working With Front-end Technology

In tech, there are several technologies at each layer of the stack. The nuances are usually not covered by tech hiring, and this is usually divided into front-end and back-end. Today, my intention is to examine the two, especially front-end technology. And here are five reasons why you might not want to make front-end technology your career.

1. Rapid evolution

There are several aspects to front-end technology. There's your standard HTML, CSS and JavaScript... and that alone can be a nightmare to keep up with. HTML, if you're up to speed with the most commonly used features of HTML5, not so much. For CSS, the preprocessor packages and CSS3 updates can be slightly more of a pain in the ass.

Multiplying like guppies

But JavaScript? If you elect to go down that rabbit hole, JavaScript is constantly evolving. The core language alone has been through update after update, The jQuery library as well. But all the front-end frameworks - ReactJS, AngularJS, VueJS, just to name a few - are also constantly going through evolution. And new frameworks seem to be coming out of the goddamn woodwork all the time!

And that's only the tech part. The design part is certainly nothing to sniff at. Learning fonts and color schemes is only the tip of the proverbial iceberg. Design patterns and anti-patterns evolve all the time. A typical website in the 90s will look markedly different ten years later, and another ten years after that.

This sounds fun if you're a hobbyist. When you do this shit for a living, not so much.

2. Uncontrollable environment

Front-end code runs on web browsers. Each of which comes with user-controllable functionality a front-end developer may not have catered for.

Yes, you read that right. User-controllable. Not developer-controllable.

Your typical web browser may come with a Dark Mode. There may almost definitely will be controls to increase or decrease font size. This is going to mess with all your layout specifications.

What about Dark Mode?!

Add in the fact that your users may screw with your carefully crafted interface by refreshing the browser insistently, pressing the Back button and using browser functions in all sorts of unruly ways. It's certainly not like a back-end interface where there are limits to input.

3. Testing nightmare

Mobile technology in the past decade has seen a proliferation of different screen sizes - tablets and phones - in addition to the standard screen sizes. In addition, there may be differences in how certain browsers render HTML, CSS and JavaScript. These differences have largely been eliminated with the demise of Internet Explorer, but some remain.

A plethora of different devices.

All of this adds up to an almost untenable amount of user interface testing for the optimal amount of exposure to the market. It is not an enviable position.

Front-end frameworks and libraries such as Twitter Bootstrap, jQuery and such, mitigate much of the insanity. But these issues have remained through the decade and will likely continue to grow the more mobile technology evolves.

4. Vague KPIs

In back-end technology, things are binary. Input is defined, and output is defined. The output is either correct, or it is not. It either is produced quickly enough, or it is not. Things either conform to security standards, or they do not.

Back-end input and
output is simpler.

Front-end technology has all that, true. It also encompasses a whole host of KPIs that are, for the most part, subjective in nature. User experience. Aesthetic beauty. Ease of use. These are guaranteed to vary from user to user and therefore, there is no way to obtain a perfect score.

Imagine being in a situation where you have to live with having less than total success. That is front-end technology.

5. Everyone has an opinion

This is somewhat related to the last point. In back-end technology, laypeople generally have better sense than to ever touch this, because they have little to no understanding of the stack and they have no tech qualifications.

Everyone and their dog.

In front-end technology, however, which is mostly visual, suddenly everybody and their dog has an opinion. Suddenly, everyone feels the need to chime in on how stuff should look and feel like. Nobody is shy about commenting because, well, you don't need tech qualifications to talk about something which is largely visual. It's subjective, so there are no wrong answers.

This makes your work a lot more difficult, when you have to take so much extra feedback into account. And when a sizeable portion of that feedback comes from laypeople, this makes things even more difficult because laypeople don't quite speak the same language techies do, and they may not have a good grasp of what technically makes sense and what doesn't.

In conclusion

Front-end technology is far, far from a bed of roses.

Still, if it still looks intriguing, let nothing deter you from your path. The important thing is not to go in blind, and be aware of all the pitfalls that await you.

Just being up front!
T___T

Friday, 11 February 2022

Web Tutorial: The Valentine Letter

Happy upcoming Valentine's Day, ya crazy lovebirds!

It's my pleasure, as always, to present the annual Valentine's Day web tutorial. This one is in standard HTML, CSS and JavaScript, and it will be a simple animation complete with graphics and a sappy love poem. We also want this animation to appear differently according to the time of day.

Ready? Here we go...

The images

For this, we will be using two pairs of backgrounds and one background for the love poem.

These are the sky backgrounds. They have been given a color adjustment in Photoshop, but are otherwise identical.

topbg_day.jpg

topbg_night.jpg



These are the earth backgrounds. They are PNG files with transparency.
middlebg_day.png

middlebg_night.png



This is the background image for the letter.

letterbg.jpg



The starting HTML is as follows.
<!DOCTYPE html>
<html>
    <head>
        <title>Valentine Letter</title>

        <style>

        </style>

        <script>

        </script>
    </head>

    <body>

    </body>
</html>


We set divs to have a translucent red background because there is going to be a fair amount of nesting.
<style>
    div {background: rgba(255, 0, 0, 0.2); }
</style>


Here, we define the function getTimeBg(). Leave it blank, for now.
<script>
    function getTimeBg()
    {

    }

</script>


The body needs to call this function upon loading.
<body onload="getTimeBg()">


Now for the divs! The first div has a CSS class of container.
<body onload="getTimeBg()">
    <div class="container">

    </div>

</body>


This is the styling for container. We want a specific width and height, and it will be set in the middle of the screen via the margin property. We will also give it a nice pink outline.
<style>
    div {background: rgba(255, 0, 0, 0.2); }

    .container
    {
        width: 800px;
        height: 700px;
        margin: 0 auto 0 auto;
        outline: 1px solid rgba(255, 100, 100, 1);
        overflow: hidden;
    }

</style>


Looking a little basic, right?


Now, add in two divs, wth CSS classes top and middle.
<div class="container">
    <div class="top">

    </div>

    <div class="middle">

    </div>

</div>


Here's the styling. They have 100% widths, but different heights. Also, middle has background properties that will fill the entire div. For now, we will leave the background image itself unspecified.
<style>
    div {background: rgba(255, 0, 0, 0.2); }

    .container
    {
        width: 800px;
        height: 700px;
        margin: 0 auto 0 auto;
        outline: 1px solid rgba(255, 100, 100, 1);
    }

    .top
    {
        width: 100%;
        height: 200px;
    }

    .middle
    {
        width: 100%;
        height: 850px;
        margin-top: -200px;
        background-size: cover;
        background-position: middle top;
        background-repeat: no-repeat;
    }

</style>


You can see where they line up.




Within the div styled using CSS class top, we have a div styled using CSS class sky.
<div class="top">
    <div class="sky" id="sky"></div>
</div>


This is the styling for sky. It takes up 400% of its parents width. The background-repeat property is set to repeat-x, because we want the sky background to stretch on forever horizontally.
.top
{
    width: 100%;
    height: 200px;
}

.sky
{
    width: 400%;
    height: 100%;
    background-position: left top;
    background-repeat: repeat-x;
}


See how this works? Don't worry about the content overflowing for the time being; I want you to have a clear visual of what is going on.




Now within the div styled by middle, we have another div styled using letter.
<div class="middle" id="earth">
    <div class="letter">

    </div>

</div>


Here are the CSS styles. letter has a specific width and height, and is set to the middle of its parent using the margin property. Its background is letterbg.jpg, and we set other properties to ensure it fills up the entire thing.
.sky
{
    width: 400%;
    height: 100%;
    background-position: left top;
    background-repeat: repeat-x;
    margin-left: 0px;    
}    

.middle
{
    width: 100%;
    height: 850px;
    margin-top: -200px;
    background-size: cover;
    background-position: middle top;
    background-repeat: no-repeat;
}

.letter
{
    width: 400px;
    height: 500px;
    margin: 0 auto 0 auto;
    background: url("letterbg.jpg");
    background-size: cover;
    background-position: middle top;
    background-repeat: no-repeat;
}


So far so good. We may need to make more adjustments later.




Yet aother div!
<div class="middle">
    <div class="letter">
        <div class="text">

        </div>    

    </div>
</div>


text
is for containing the poem, so we will make it even less wide than letter, set it in the middle using the margin property, and set font sizes.
.letter
{
    width: 400px;
    height: 500px;
    margin: 0 auto 0 auto;
    background: url("letterbg.jpg");
    background-size: cover;
    background-position: middle top;
    background-repeat: no-repeat;
}

.text
{
    width: 80%;
    margin: 0 auto 0 auto;
    font-family: georgia;
    font-size: 12px;
    font-weight: bold;
    line-height: 2.5em;
}


Now we will add two p tags within the div styled using text. They will be styled using poem and author, respectively.
<div class="middle">
    <div class="letter">
        <div class="text">
            <p class="poem">

            </p>
            <p class="author">

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


Fill up the first p tag with the poem, and the second p tag with the author's name.
<div class="middle">
    <div class="letter">
        <div class="text">
            <p class="poem">
                <br />
                How do I love thee? Let me count the ways.<br />
                I love thee to the depth and breadth and height<br />
                My soul can reach, when feeling out of sight<br />
                For the ends of being and ideal grace.<br />
                I love thee to the level of every day's<br />
                Most quiet need, by sun and candle-light.<br />
                I love thee freely, as men strive for right.<br />
                I love thee purely, as they turn from praise.<br />
                I love thee with the passion put to use<br />
                In my old griefs, and with my childhood's faith.<br />
                I love thee with a love I seemed to lose<br />
                With my lost saints. I love thee with the breath,<br />
                Smiles, tears, of all my life; and, if God choose,<br />
                I shall but love thee better after death.<br />
            </p>
            <p class="author">
                - Elizabeth Barret Browning
            </p>

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


Let's style those p tags. This is just for aesthetics.
.text
{
    width: 80%;
    margin: 0 auto 0 auto;
    font-family: georgia;
    font-size: 12px;
    font-weight: bold;
    line-height: 2.5em;
}

.poem
{
    font-style: italic;
}

.author
{
    text-align: right;
}


Here we go!




The backgrounds

First, we add ids to the divs that we need to set backgrounds for.
<div class="top">
    <div class="sky" id="sky"></div>
</div>

<div class="middle" id="earth">


Next, we add in some script. We first get sky and earth using the getElementById() method. Then we get the date using new Date() and assign it to the variable d.
function getTimeBg()
{
    var sky = document.getElementById("sky");
    var earth = document.getElementById("earth");

    var d = new Date();

}


Set an If block, that examines the output of the getHours() method. If the time of the day is between 6 AM and 6 PM (i.e, d.getHours() is between 6 to 18 inclusive), we use the day versions of the backgrounds. If not, we use the night versions.
function getTimeBg()
{
    var sky = document.getElementById("sky");
    var earth = document.getElementById("earth");

    var d = new Date();
    if (d.getHours() >= 6 && d.getHours() <= 18) {
        sky.style.background = "url(topbg_day.jpg)";
        earth.style.background = "url(middlebg_day.png)";
    }
    else
    {
        sky.style.background = "url(topbg_night.jpg)";
        earth.style.background = "url(middlebg_night.png)";
    }

}


It's now 11 PM where I am, so the night versions are on!




At this point, we should be turning off the background color of the divs.
div {background: rgba(255, 0, 0, 0); }


Also, add breaks here to adjust the content. It's a really lazy way, I know.
<div class="middle" id="earth">
    <br /><br /><br /><br /><br /><br /><br /><br /><br />
    <div class="letter">






Animating the sky
For this, we will use CSS3 animations. This will go right into the CSS class sky. We use an animation name, skymove, which we will expand on later, and set a duration. Make this fast, maybe 1 second, so we can check on the smoothness of the animation. The iteration count is set to infinite so that it will go on forever, and we'll set the timing function to linear so that there won't be any acceleration or deceleration of the animation. It will be at a constant speed.
.sky
{
    width: 400%;
    height: 100%;
    background-position: left top;
    background-repeat: repeat-x;
    margin-left: 0px;
    animation-name: skymove;
    animation-duration: 1s;    
    animation-iteration-count: infinite;
    animation-timing-function: linear;    

}


Now we will define skymove. At 100%, the margin-left property is set to minus twice the width of the background, which is minus 1600 pixels.
.sky
{
    width: 400%;
    height: 100%;
    background-position: left top;
    background-repeat: repeat-x;
    margin-left: 0px;
    animation-name: skymove;
    animation-duration: 1s;    
    animation-iteration-count: infinite;
    animation-timing-function: linear;    
}

@keyframes skymove
{
    0%   {margin-left: 0px;}
    100% {margin-left: -1600px;}
}        


Now the sky div moves left! You can see that the animation appears infinite due to the background-repeat property being set to repeat-x.




Set the overflow property of these classes to hidden.
.container
{
    width: 800px;
    height: 700px;
    margin: 0 auto 0 auto;
    outline: 1px solid rgba(255, 100, 100, 1);
    overflow: hidden;
}

.top
{
    width: 100%;
    height: 200px;
    overflow: hidden;
}


Ah, great stuff! Now that the overflow property is set to hidden, it looks like the sky is animated! You may want to make the animation slower, maybe set the duration to 200 seconds or something.




Final loving words

I know I know, this Valentine's day web tutorial is kind of corny. But hey, they can't all be winners. And let's face it, Valentine's Day can be pretty corny.

Did the earth sky move for you? Heh heh.
T___T

Friday, 2 April 2021

Web Tutorial: VueJS Easter Puzzle

What's up, you glorious geeks? Easter approaches!

Traditionally, at this time of year, we have our Easter-themed web tutorial. This year, I shall not disappoint... and there's a special treat because I will be using VueJS for this one.

My usage of the framework will be a very rudimentary one, just to get beginners off on the right foot. What we will be doing is a very simple puzzle game. We will use an Easter-themed image, like this one. It's a 500 by 500 pixels square image.


easter.jpg

Now imagine if the image was scrambled and you had to unscramble it within a time limit. That will be what our game does!

The setup

As usual, it all begins with some HTML.
<!DOCTYPE html>
<html>
    <head>
        <title>Easter Puzzle</title>

        <style>

        </style>
    </head>

    <body>
        <script>

        </script>
    </body>
</html>


Here, let's add something to the styling - a red outline to all divs for dev purposes.
<!DOCTYPE html>
<html>
    <head>
        <title>Easter Puzzle</title>

        <style>
            div { outline: 0px solid #FF0000;}
        </style>
    </head>

    <body>
        <script>

        </script>
    </body>
</html>


The next few items are specific to VueJS. We need a div with an id of easterApp. And then we need to link the VueJS library. Finally, we begin scripting by declaring a variable app, and setting it to a new instance of the Vue object.
<html>
    <head>
        <title>Easter Puzzle</title>

        <style>
            div { outline: 1px solid #FF0000;}
        </style>
    </head>

    <body>
        <div id="easterApp">

        </div>


        <script src="https://cdnjs.cloudflare.com/ajax/libs/vue/1.0.18/vue.min.js"></script>

        <script>
            var app = new Vue
            (

            );
        </script>

    </body>
</html>


We then fill in app with an object within the new Vue object. It will have the properties el (which we will set to the id easterApp to let Vue know which DOM element to reference), methods to hold the different methods for this app, and created to define what gets run as soon as the app loads.
<script>
    var app = new Vue
    (
        {
            el: "#easterApp",
            data:
            {

            },
            methods:
            {

            },
            created: function()
            {

            }
        }

    );
</script>


Inside easterApp, add a div with an id of timeContainer. Inside that, put some HTML. These are elements that will be populated later. And then we'll also have a div with an id of puzzleContainer right under timeContainer.
<div id="easterApp">
    <div id="timeContainer">
        <h2>Message</h2>
        <h1>Seconds</h1>
        <button>ButtonText</button>
    </div>

    <div id="puzzleContainer">

    </div>

</div>


Let's style! easterApp will be 500 pixels wide. We'll use the margin property to set it center of the screen, and then set some font styling.
<style>
    div { outline: 1px solid #FF0000;}

    #easterApp
    {
        width: 500px;
        margin: 0 auto 0 auto;
        font-family: verdana;
        font-size: 12px;
    }

</style>


Next is timeContainer. It will take up full width of easterApp. We give it 150 pixels in height, and align text center.
<style>
    div { outline: 1px solid #FF0000;}

    #easterApp
    {
        width: 500px;
        margin: 0 auto 0 auto;
        font-family: verdana;
        font-size: 12px;
    }

    #timeContainer
    {
        width: 100%;
        height: 150px;
        text-align: center;
    }

</style>


Here, I'm going to style the button. Use whatever fancy CSS you like, but I'm gonna go with orange background and white text.
<style>
    div { outline: 1px solid #FF0000;}

    #easterApp
    {
        width: 500px;
        margin: 0 auto 0 auto;
        font-family: verdana;
        font-size: 12px;
    }

    #timeContainer
    {
        width: 100%;
        height: 150px;
        text-align: center;
    }

    #timeContainer button
    {
        width: 8em;
        height: 2em;
        text-align: center;
        background-color: rgba(255, 100, 0, 1);
        color: rgba(255, 255, 255, 1);
        font-weight: bold;
        border-radius: 10px;
        border: 0px solid black;
    }

    #timeContainer button:hover
    {
        background-color: rgba(255, 100, 0, 0.5);
        color: rgba(255, 0, 0, 1);
    }

</style>


And puzzleContainer will be a square, so we give it a height of 500 pixels.
<style>
    div { outline: 1px solid #FF0000;}

    #easterApp
    {
        width: 500px;
        margin: 0 auto 0 auto;
        font-family: verdana;
        font-size: 12px;
    }

    #timeContainer
    {
        width: 100%;
        height: 150px;
        text-align: center;
    }

    #timeContainer button
    {
        width: 8em;
        height: 2em;
        text-align: center;
        background-color: rgba(255, 100, 0, 1);
        color: rgba(255, 255, 255, 1);
        font-weight: bold;
        border-radius: 10px;
        border: 0px solid black;
    }

    #timeContainer button:hover
    {
        background-color: rgba(255, 100, 0, 0.5);
        color: rgba(255, 0, 0, 1);
    }

    #puzzleContainer
    {
        width: 100%;
        height: 500px;
    }

</style>


This looks like a good start, wouldn't you say?


In the data object, fill in these properties. timer is set to undefined by default. We have seconds with a value of 100 (which is perhaps a bit too much, but you can tweak this later) and btnText, whose default value is "RESET". For message, set it to "Time elapsed". pieces will be an empty array.
data:
{
    timer: undefined,
    seconds: 100,
    btnText: "RESET",
    message: "Time elapsed",
    pieces: []

},


Within the created() method, make a call to the reset() method. This means that as soon as the app loads, reset() will run.
created: function()
{
    this.reset();
}


You'll notice that reset() is prefixed by a reference to this. That's because reset() is a method within the same object that created() is a child of. We will create reset() now, under methods.
methods:
{
    reset: function()
    {

    },

},


In here, we set the properties in data to their default values.
methods:
{
    reset: function()
    {
        this.seconds = 100;
        this.btnText = "RESET";
        this.message = "Time elapsed";
        this.pieces = [];

    },
},


Then we insert calls to stopTimer() and startTimer().
methods:
{
    reset: function()
    {
        this.stopTimer();
        this.seconds = 100;
        this.btnText = "RESET";
        this.message = "Time elapsed";
        this.startTimer();
        this.pieces = [];
    },
},


Next, we create these methods.
reset: function()
{
    this.stopTimer();
    this.seconds = 100;
    this.btnText = "RESET";
    this.message = "Time elapsed";
    this.startTimer();
    this.pieces = [];
},
startTimer: function()
{

},
stopTimer: function()
{

},


For stopTimer(), we use timer as an argument in the clearInterval() function. This means, basically, that we stop the timer, and then we set timer to undefined.
stopTimer: function()
{
    clearInterval(this.timer);
    this.timer = undefined;

},


For startTimer(), this only fires off if timer is undefined. And it would be if stopTimer() was run.
startTimer: function()
{
    if (this.timer == undefined)
    {

    }

},


If timer is undefined, set timer by running setInterval with an interval of 1 second.
startTimer: function()
{
    if (this.timer == undefined)
    {
        this.timer = setInterval
        (
            () =>
            {

            },
            1000
        );

    }
},


In here, decrement seconds.
startTimer: function()
{
    if (this.timer == undefined)
    {
        this.timer = setInterval
        (
            () =>
            {
                this.seconds = this.seconds - 1;
            },
            1000
        );
    }
},


Now check for the value of seconds. If it's reached the value of 0, run stopTimer(). Then set btnText to "REPLAY" and set message.
startTimer: function()
{
    if (this.timer == undefined)
    {
        this.timer = setInterval
        (
            () =>
            {
                this.seconds = this.seconds - 1;

                if (this.seconds == 0)
                {
                    this.stopTimer();
                    this.btnText = "REPLAY";
                    this.message = "Better luck next time!";
                }

            },
            1000
        );
    }
},


Now in the HTML, replace the text we put there, with template strings.
<div id="timeContainer">
    <h2>{{message}}</h2>
    <h1>{{seconds}}</h1>
    <button>{{btnText}}</button>
</div>


Add a click event handler using v-on, and make it call reset().
<div id="timeContainer">
    <h2>{{message}}</h2>
    <h1>{{seconds}}</h1>
    <button v-on:click="reset">{{btnText}}</button>
</div>


Now you'll see that there's a number there where "seconds" was, and it's counting down each second! Also, there's a "REPLAY" button now. If you click it, reset() will be called and the seconds display will go back to 100, then start counting down again.


And once it reaches 0, the message changes!


Next, we're going to populate the puzzle. It will be a 5 by 5 square grid. So create a nested For loop like this. 5 rows, 5 columns.
reset: function()
{
    this.stopTimer();
    this.seconds = 100;
    this.btnText = "RESET";
    this.message = "Time elapsed";
    this.startTimer();
    this.pieces = [];

    for (var row = 0; row < 5; row++)
    {
        for (var col = 0; col < 5; col++)
        {

        }
    }

},


Because each square is going to be 25 by 25 pixels, declare offsetX as the product of col by 25. offsetY will be the product of row by 25.
for (var row = 0; row < 5; row++)
{
    for (var col = 0; col < 5; col++)
    {
        var offsetX = col * 25;
        var offsetY = row * 25;
    }
}


Declare a temporary object, piece. The id property will be unique and going by that formula, it will be from 0 to 24. Set the rotation property to 0, for now. And then set the offsetX and offsetY properties to the current value of offsetX and offsetY, respectively.
for (var row = 0; row < 5; row++)
{
    for (var col = 0; col < 5; col++)
    {
        var offsetX = col * 25;
        var offsetY = row * 25;

        var piece =
        {
            id: (row * 5) + col,
            rotation: 0,
            offsetX: offsetX,
            offsetY: offsetY
        }
    }
}


And finally, push piece into the pieces array. So by the time the nested For loop is done, you should have 25 elements in the pieces array, each with a different offsetX and offsetY value.
for (var row = 0; row < 5; row++)
{
    for (var col = 0; col < 5; col++)
    {
        var offsetX = col * 25;
        var offsetY = row * 25;

        var piece =
        {
            id: (row * 5) + col,
            rotation: 0,
            offsetX: offsetX,
            offsetY: offsetY
        }

        this.pieces.push(piece);
    }
}


In the HTML, inside puzzleContainer, create a div with a class of pieceContainer, and use v-for to ensure that this repeats for every element in pieces.
<div id="puzzleContainer">
    <div class="pieceContainer" v-for="piece in pieces">

    </div>

</div>


Now in the CSS, add the pieceContainer CSS class. The width and height will be 20% of puzzleContainer, which is 25 pixels. And we set the float property to left.
#puzzleContainer
{
    width: 100%;
    height: 500px;
}

.pieceContainer
{
    width: 20%;
    height: 20%;
    float: left;
}


At this point, you should have a 5 by 5 square grid, each square 25 pixels in height and width.


Back to the HTML, within each div styled using pieceContainer, insert a div styled using piece.
<div id="puzzleContainer">
    <div class="pieceContainer" v-for="piece in pieces">
        <div class="piece">

        </div>

    </div>
</div>


Use v-bind to set the data-id property to the id property of piece.
<div id="puzzleContainer">
    <div class="pieceContainer" v-for="piece in pieces">
        <div class="piece" v-bind:data-id="piece.id">

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


Let's add something to the CSS - the piece class. Each of these will fill full width and height of its parent, and its background image will be easter.jpg.
.pieceContainer
{
    width: 20%;
    height: 20%;
    float: left;
}

.piece
{
    width: 100%;
    height: 100%;
    background-image: url(easter.jpg);
    background-repeat: no-repeat;
}


And there it is...


Now use v-bind on the style attribute. The value will be what's returned from the getStyle() method with piece passed in as an argument. We'll create that next.
<div id="puzzleContainer">
    <div class="pieceContainer" v-for="piece in pieces">
        <div class="piece" v-bind:data-id="piece.id" v-bind:style="getStyle(piece)">

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


Inside the methods object, create the getStyle() method. It will accept obj as a parameter.
stopTimer: function()
{
    clearInterval(this.timer);
    this.timer = undefined;
},
getStyle: function(obj)
{

},


In here, we will generate CSS. Declare rotate and set it to some CSS code. We use the rotation property (which is currently 0 for all pieces).
getStyle: function(obj)
{
    var rotate = "transform: rotate(" + obj.rotation + "deg)";
},


Then we declare offset and do something similar, only this time it's for the background-position property, and we use the offsetX and offsetY properties of obj to generate the CSS.
getStyle: function(obj)
{
    var rotate = "transform: rotate(" + obj.rotation + "deg)";
    var offset = "background-position:" + obj.offsetX + "% " + obj.offsetY + "%";
},


And finally, we return the full CSS style string.
getStyle: function(obj)
{
    var rotate = "transform: rotate(" + obj.rotation + "deg)";
    var offset = "background-position:" + obj.offsetX + "% " + obj.offsetY + "%";

    return rotate + ";" + offset;
},


And now we have a full picture.


Let's randomly rotate all the pieces now. Get back to the reset() method. Inside the inner loop of the nested For loop, declare randomRotation. There are 4 possibilities - from 0 to 3. Then multiply by 90 to get the degree of rotation. Finally, change rotation within the temporary piece object to randomRotation.
for (var row = 0; row < 5; row++)
{
    for (var col = 0; col < 5; col++)
    {
        var randomRotation = Math.floor(Math.random() * 3) * 90;
        var offsetX = col * 25;
        var offsetY = row * 25;

        var piece =
        {
            id: (row * 5) + col,
            rotation: randomRotation,
            offsetX: offsetX,
            offsetY: offsetY
        }

        this.pieces.push(piece);
    }
}


Now this is what it should look like, with pieces all rotated.


Sweet! Now we will add something for the user to be able to rotate the pieces on their own. Use v-on to add a click event, and set it to call the rotatePiece() method.
<div class="piece" v-bind:data-id="piece.id" v-bind:style="getStyle(piece)" v-on:click="rotatePiece">

</div>


Obviously, we will then add this method under methods.
getStyle: function(obj)
{
    var rotate = "transform: rotate(" + obj.rotation + "deg)";
    var offset = "background-position:" + obj.offsetX + "% " + obj.offsetY + "%";

    return rotate + ";" + offset;
},
rotatePiece: function(e)
{

},


This will only fire off if timer is undefined, which means the game is in progress.
rotatePiece: function(e)
{
    if (this.timer != undefined)
    {
                                
    }

},


Declare piece. Set it to the element in pieces pointed to by the id property in the event's element. Remember we set the data-id property? Yep, so we're going to make some changes to piece, then reassign the value back to the element of the pieces array.
rotatePiece: function(e)
{
    if (this.timer != undefined)
    {
        var piece = this.pieces[e.target.dataset.id];

        this.pieces[piece.id] = piece; 
                               
    }
},


Now bear in mind that we can only rotate 90 degrees clockwise. That will be a limit we set. Use an If-else block. Check if the rotation property is 270.

rotatePiece: function(e)
{
    if (this.timer != undefined)
    {
        var piece = this.pieces[e.target.dataset.id];

        if (piece.rotation == 270)
        {

        }
        else
        {

        }


        this.pieces[piece.id] = piece;                                
    }
},


So if the rotation is at 270 degrees, one more rotation would put it back at 0 degrees. Otherwise, just add 90 to the current value of the rotation property.
rotatePiece: function(e)
{
    if (this.timer != undefined)
    {
        var piece = this.pieces[e.target.dataset.id];

        if (piece.rotation == 270)
        {
            piece.rotation = 0;
        }
        else
        {
            piece.rotation = piece.rotation + 90;
        }

        this.pieces[piece.id] = piece;                                
    }
},


Now add a new method - checkIncorrectPieces().
rotatePiece: function(e)
{
    if (this.timer != undefined)
    {
        var piece = this.pieces[e.target.dataset.id];

        if (piece.rotation == 270)
        {
            piece.rotation = 0;
        }
        else
        {
            piece.rotation = piece.rotation + 90;
        }

        this.pieces[piece.id] = piece;                                
    }
},
checkIncorrectPieces: function()
{

}


Basically, we declare incorrect, then set it to a new array derived from running pieces through the filter() method, returning all those elements where the rotation property is not 0. Because 0 is the correct value. After that, we return the number of incorrect values.
checkIncorrectPieces: function()
{
    var incorrect = this.pieces.filter((x) => { return x.rotation != 0;});

    return incorrect.length;

}


So every time the timer runs, it should check if the number of incorrect pieces is 0, which means that all the pieces have been correctly rotated. When that happens, we run the stopTimer() method, set btnText and message, and this effectively ends the game.
startTimer: function()
{
    if (this.timer == undefined)
    {
        this.timer = setInterval
        (
            () =>
            {
                this.seconds = this.seconds - 1;

                if (this.seconds == 0)
                {
                    this.stopTimer();
                    this.btnText = "REPLAY";
                    this.message = "Better luck next time!";
                }

                if (this.checkIncorrectPieces() == 0)
                {
                    this.stopTimer();
                    this.btnText = "REPLAY";
                    this.message = "Congratulations!";
                }

            },
            1000
        );
    }
},


Now when you rotate all the pieces correctly, the timer stops moving and you get a congratulatory message!


A few final touches!

Add this to the CSS so that the animation we're about to add, comes out nicely.
#puzzleContainer
{
    width: 100%;
    height: 500px;
    transition: all 1s;
}


Bind the class attribute using v-bind. If checkIncorrectPieces() is 0, then the class is win.
<div id="puzzleContainer" v-bind:class="checkIncorrectPieces() == 0 ? 'win' : ''">
    <div class="pieceContainer" v-for="piece in pieces">
        <div class="piece" v-bind:data-id="piece.id" v-bind:style="getStyle(piece)" v-on:click="rotatePiece">

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


Add win to the CSS. It will be a nice thick orange outline.
.piece
{
    width: 100%;
    height: 100%;
    background-image: url(easter.jpg);
    background-repeat: no-repeat;
}

.win
{
    outline: 5px solid rgba(255, 100, 0, 1);
}


Now when you win the game, you get this orange outline fading in! If you click the REPLAY button, it should fade out again.


Finally, remove the red outline.
div { outline: 0px solid #FF0000;}


And this is your final product.


Final Notes

I do like VueJS, or at least, I like the version we're using and how we're using it. The structure is pretty much how I usually write my JavaScript anyway, and it combines the best features of AngularJS and ReactJS without being a pain in the ass about it.

Piece be with you!
T___T