Χιονονιφάδες! (σε javascript)

Ψαχουλεύοντας κάποια παλιά μου projects, έπεσα πάνω σε αυτό το προγραμματάκι. Ήταν η προσπάθειά μου να πειραματιστώ με τους constructors και τα prototypes της javascript. Ήταν ένα κρύο απόγευμα, σαν κι απόψε, γι αυτό σκέφτηκα να “χιονίσω” μετρικά html elements…

Let there be snow!

Η ιδέα ήταν να φτιάξω ένα object για κάθε χιονονιφάδα, που θα οπτικοποιείται με ένα μικρό div που θα περιέχει ένα χαρακτήρα χιονονιφάδας. Στην αρχή σκέφτηκα να χρησιμοποιήσω αστερίσκους, μέχρι που ανακάλυψα ότι υπάρχουν τρία html entities, ειδικά για την περίσταση, με δεκαεξαδικούς κωδικούς 0x2744 έως και 0x2746. Τώρα είχα τρεις διαφορετικούς τύπους χιονονιφάδας, που επιπλέον μπορούσα να τους ορίσω με css:

.flake
    {
	position: absolute;
	color: white;
	background-color: transparent;
	}
.flake.type1:after { content: "\2744"; }
.flake.type2:after { content: "\2745"; }
.flake.type3:after { content: "\2746"; }
	

Θέλω να μπορώ να κάνω οποιοδήποτε element να χιονίζει, ακόμα και πολλά συγχρόνως στην ίδια σελίδα. Συνεπώς, χρειάζομαι έναν πίνακα από “χιονισμένα elements” (snowingEls). Επίσης, το βρίσκω βολικό να υπάρχει ένας κεντρικός πίνακας για όλα τα αντικείμενα χιονονιφάδας (snowflakes). Μαζί με κάποιες άλλες εξ ορισμού τιμές, ο γενικός αλγόριθμος είναι πάνω-κάτω ο ακόλουθος:

var 
    snowingEls={},
	snowflakes=[],
	minSize=10,
	maxSize=30,
	speed=1, 
	vspeed=speed/2,
	hspeed=3;
	
function snow(el,nFlakes)
    {
	if(typeof snowingEls[el.id]==='undefined')
		snowingEls[el.id]={
			curFlakes: 0,
			};
	snowingEls[el.id].nFlakes=nFlakes;
	snowingEls[el.id].lowestFlake=0;
	if(nFlakes) new Snowflake(el);
	}

setInterval(
    function()
		{
		moveFlakes();
		addFlakes();
		renderFlakes();
		},
	50
	);
	

Για να κάνουμε ένα element να χιονίζει, θα πρέπει να καλέσουμε τη συνάρτηση snow() δίνοντας σαν παραμέτρους το επιθυμητό element και τον αριθμό των χιονονιφάδων που θέλουμε να περιέχει. Η συνάρτηση θα προσθέσει το element στον πίνακα των “χιονισμένων”, αν δεν υπάρχει ήδη, και θα το αρχικοποιήσει. Τέλος, θα δημιουργήσει την πρώτη χιονονιφάδα μέσα σε αυτό.

Υπάρχει επίσης κι ένα timer για να δώσει κίνηση στις χιονονιφάδες. Αλλά θα φτάσουμε σ’ αυτό αργότερα. Για τώρα, ας δούμε τον ορισμό του object χιονονιφάδας:

function Snowflake(el)
    {
	this.el=el;
	this.flakeEl=document.createElement('div');
	el.appendChild(this.flakeEl);
	this.init();
	snowflakes.push(this);
	snowingEls[el.id].curFlakes++;
	}
	
Snowflake.prototype.init=function()
	{
	this.speed=speed+Math.random()*vspeed;
	flake=this.flakeEl;
	flake.setAttribute('class','flake type'+Math.floor((Math.random()*3)+1));
	this.size=minSize+Math.random()*(maxSize-minSize);
	this.x=Math.floor((Math.random()*this.el.clientWidth)+1);
	this.y=-this.size;
	this.hdir=hspeed*(Math.random()-0.5);
    flake.style.fontSize=this.size+'px';
	}
	
Snowflake.prototype.render=function()
	{
	s=this.flakeEl.style;
	s.left=this.x+'px';
	s.top=this.y+'px';
	}
	
Snowflake.prototype.dispose=function()
	{
	snowingEls[this.el.id].curFlakes--;
	this.flakeEl.parentNode.removeChild(this.flakeEl);
	snowflakes.splice(snowflakes.indexOf(this),1);
	}
	

Ο constructor μας θα κατασκευάσει και το div της χιονονιφάδας και θα προσθέσει όλο το αντικείμενο στον πίνακα των χιονονιφάδων. Παρόλο που δεν χρειαζόμαστε destructor, λόγω του garbage collection της javascript, συμπεριέλαβα μία dispose() για να μπορούμε να ξεφορτωθούμε το div και να αφαιρέσουμε τη χιονονιφάδα από τον πίνακα όταν πλέον δεν θα τη χρειαζόμαστε.

Υπάρχει επίσης μία ξεχωριστή init() που καλείται από τον constructor. Αυτό, γιατί θέλουμε να “ανακυκλώνουμε” τις χιονονιφάδες, και αντί να τις καταστρέφουμε και να δημιουργούμε νέες, απλά επαναρχικοποιούμε τις παλιές. Αυτή η συνάρτηση τοποθετεί τη χιονονιφάδα στην κορυφή του element που την περιέχει και δίνει τυχαίες αρχικές τιμές στα διάφορα πεδία που την καθορίζουν. Σε δεδομένη χρονική στιγμή, η χιονονιφάδα έχει συγκεκριμένη τοποθεσία που ορίζεται από δυο συντεταγμένες, τις x και y. Επίσης έχει ένα μέγεθος (size) και μία ταχύτητα πτώσης (speed). Αλλά το χιόνι δεν πέφτει κατακόρυφα, επομένως χρειαζόμαστε και μία ταχύτητα στον οριζόντιο άξονα που θα δώσει στη χιονονιφάδα μία δεξιά ή αριστερή κατεύθυνση (hdir).

Τέλος, η render() θα ενημερώνει το div της χιονονιφάδας με τη θέση της, από τα πεδία του αντικειμένου, ώστε να οπτικοποιηθεί η κίνηση.

Τώρα, ας εξετάσουμε τα κομμάτια που λείπουν: τις τρεις συναρτήσεις που καλούνται από το timer.

Η τελευταία, η renderFlakes(), απλά διατρέχει τον πίνακα των χιονονιφάδων και καλεί τη render() για κάθε αντικείμενο:

function renderFlakes()
    {
	// render all flakes
	for(i=0;i<snowflakes.length;i++) snowflakes[i].render();
	}
	

Για να μετακινήσουμε τις χιονονιφάδες θα πρέπει επίσης να διατρέξουμε τον πίνακα. Κάθε χιονονιφάδα θα χαμηλώσει ανάλογα με την κατακόρυφη ταχύτητα που της έχει ανατεθεί. Πέρα από την ενημέρωση του πεδίου lowestFlake, που θα εξετάσουμε παρακάτω, θα πρέπει επίσης να ελέγξουμε αν η θέση της ξεπερνά το κάτω όριο του element που την περιέχει. Σε τέτοια περίπτωση, θα πρέπει να αποφασίσουμε αν θα την ανακυκλώσουμε ή θα την καταστρέψουμε. Η λογική είναι απλή: Αν υπάρχουν περισσότερες από τις αναμενόμενες, τότε θα πρέπει να διαγράψουμε κάποιες.

function moveFlakes()
    {
	// advance each snowflake
	for(i=0;i<snowflakes.length;i++)
		{
		flake=snowflakes[i];
		container=snowingEls[flake.el.id];
		flake.y+=flake.speed;
		if(flake.y>container.lowestFlake)
			container.lowestFlake=flake.y;
		if(flake.y<flake.el.clientHeight) 
			flake.x+=flake.hdir;
		else
			if(container.curFlakes>container.nFlakes)
				flake.dispose();
			else
				flake.init();
		}
	}
	

Άφησα το δυσκολότερο μέρος τελευταίο. Θα παρατηρήσατε ότι δεν δημιουργούμε όλες τις χιονονιφάδες στην αρχή. Αν το κάναμε, θα έπεφταν όλες σχεδόν μαζί, και θα υπήρχε κενό μέχρι να δημιουργηθεί το επόμενο “κύμα”. Αντίθετα, δημιουργούμε μόνο την πρώτη, ώστε να αρχίσει να ενημερώνεται το lowestFlake. Αυτό το πεδίο σηματοδοτεί ποιο κομμάτι του κάθετου διαστήματος έχει χιονονιφάδες και ποιο είναι ακόμη άδειο. Επομένως, αν το x% του χώρου έχει χιόνι, ο αριθμός των χιονονιφάδων που θα πρέπει να έχουν δημιουργηθεί πρέπει να φτάσει το x% του συνόλου.

Όταν έρθει η ώρα υπολογίζουμε αυτό το ποσοστό, όπως και το ποσοστό των χιονονιφάδων προς το επιδιωκόμενο σύνολο. Τότε, μπορούμε να αποφασίσουμε πόσες νιφάδες χρειάζεται να δημιουργηθούν ώστε τα ποσοστά να είναι τα ίδια. Αυτό κάνει το χιόνι να καλύπτει ομοιόμορφα το element. Σε συνδυασμό με την οριζόντια ταχύτητα, δημιουργεί μία ρεαλιστική εντύπωση χιονόπτωσης.

function addFlakes()
    {
	// check if we need to create new ones
	for(id in snowingEls)
		{
		entry=snowingEls[id];
		el=document.getElementById(id);
		coveredHeight=entry.lowestFlake/el.clientHeight;
		coveredNum=entry.curFlakes/entry.nFlakes;
		needed=(coveredHeight-coveredNum)*entry.nFlakes;
		for(i=0;i<needed;i++) new Snowflake(el);
		}
	}
	

Τώρα, πως θα χειριστούμε την ποσότητα χιονιού κατά το χρόνο εκτέλεσης; Προφανώς, μπορούμε να καλέσουμε την snow() όσες φορές θέλουμε, για να αλλάξουμε την ποσότητα χιονιού σε ένα element. Ας το δοκιμάσουμε:

<label>
    Amount of snowflakes:
    <select id='howmany'>
        <option value='0'>0</option>
        <option value='10'>10</option>
        <option value='20'>20</option>
        <option value='50'>50</option>
        <option value='100'>100</option>
    </select>
</label>
<div id='christmas-night' style='position:relative;overflow:hidden;background-color:grey;text-align:center;'>
    <img 
        src='http://www.geomagas.gr/wp-content/uploads/2013/12/christmas_night_by_adni18-d342xs1.jpg'  
        style='width:100%;'
    />
</div>
<script type='text/javascript'>
    (function ($) {
        $('#howmany').change(function(){
            el=document.getElementById('christmas-night');
            snow(el,this.value);
            });
        $(document).ready(
            function()
    			{
                $('#howmany').val(20).trigger('change');
    			}
    		);
        })(jQuery);
</script>

Και αυτό είναι το αποτέλεσμα του παραπάνω:

Θα παρατηρήσετε ότι, οι δραστικές αλλαγές στην ποσότητα χιονιού, προκαλούν το φαινόμενο του “κενού/κύματος” που συζητήσαμε παραπάνω. Η λύση θα ήταν να αλλάζουμε την ποσότητα σταδιακά, ίσως καλώντας τη snow() μέσα από ένα άλλο timer, μέχρι να φτάσει στον επιθυμητό αριθμό. Αλλά αυτό το αφήνω σε σας.

Καλά Χριστούγεννα!

One Comment

Αφήστε μια απάντηση

Η ηλ. διεύθυνση σας δεν δημοσιεύεται. Τα υποχρεωτικά πεδία σημειώνονται με *