Raining clouds


#1

I have been working on this for a few weeks now and i think it is close to perfect now.

Its not my first project but i am curious what you guys/girls think.
Looking for feedback.

<html>
<head>
<style>
#background-canvas {
  position: fixed;
  width: 100%;
  height: 100%
  background-color:red;
  top:0;
  left:0;
}
</style>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
</head>
<body>
<canvas id="background-canvas" />
<script>
$(document).ready(function () {

    var canvas = document.getElementById("background-canvas");
    canvas.width = $(window).width();
    canvas.height = $(window).height();
    canvas.style.zIndex = -1;
    var ctx = canvas.getContext("2d");
	
	var mousePosition = new Vector2d(0,0);
	
    var background = new Background(canvas.width, canvas.height, new Color(224,247,250,0.8));
	var cloud = new Cloud(300, 100, new Vector2d(373,86), 1000, 5);
	var cloud1 = new Cloud(300, 100, new Vector2d(112,61), 1000, 5);
	var cloud2 = new Cloud(300, 100, new Vector2d(622,55), 1000, 5);
	img=new Image();
    img.src="https://i.imgur.com/hIVsoho.png";

	background.addCloud(cloud);
	background.addCloud(cloud1);
	background.addCloud(cloud2);
	
	/*
	*	Input listeners
	*/
	document.addEventListener("mousemove", function (evt) {
        mousePosition = getMousePos(canvas, evt);
    }, false);
	document.addEventListener("mousedown", function (evt){
		for(var i = 0; i < background.allClouds.length; i++)
			if(background.allClouds[i].hover(mousePosition)){
				background.allClouds[i].isClicked = true;
				background.allClouds[i].clickLocalPosition = new Vector2d(mousePosition.x, mousePosition.y);
				background.allClouds[i].clickLocalPosition.sub(background.allClouds[i].location);
			}
	}, false);
	document.addEventListener("mouseup", function (evt){
		for(var i = 0; i < background.allClouds.length; i++)
			if(background.allClouds[i].hover(mousePosition))
				background.allClouds[i].isClicked = false;
	}, false);
	
	
	setInterval(updateBackground, 20);		
	
	function updateBackground() {
		// paint background color.
		ctx.fillStyle = background.color.getColorString();
		ctx.fillRect(0,0,background.width, background.height);
		
		for(var i = 0; i < background.allClouds.length; i++){
			var selectedCloud = background.allClouds[i];
			var deadPixelContainer = [];
			
			if(selectedCloud.allPixels.length > 0){
				for (var j = 0; j < selectedCloud.allPixels.length; j++){
					var selectedPixel = selectedCloud.allPixels[j];
					// paint rain
					ctx.fillStyle = selectedPixel.color.getColorString();
					ctx.save();
					ctx.translate(selectedPixel.location.x, selectedPixel.location.y);
					ctx.fillRect(-selectedPixel.width / 2, -selectedPixel.height / 2, selectedPixel.width, selectedPixel.height);
					ctx.restore();
					
					if(!selectedPixel.alive){
						deadPixelContainer.push(selectedPixel); // mark dead pixel
						continue;
					}
					selectedPixel.update();
					selectedPixel.checkEdges(background);
					
				}
				
				// dispose of dead pixels
				if(deadPixelContainer.length > 0){
					selectedCloud.removePixels(deadPixelContainer);
				}
			
				// paint clouds
				ctx.save();
				ctx.translate(selectedCloud.location.x, selectedCloud.location.y);
				ctx.drawImage(img,0,0,img.width,img.height,-25, -10,350,100);
				ctx.restore();
			}
			
			// make cloud draggable
			if(selectedCloud.isClicked){
				selectedCloud.location.x = mousePosition.x - selectedCloud.clickLocalPosition.x;
				selectedCloud.location.y = mousePosition.y - selectedCloud.clickLocalPosition.y;	
			}
			
			// add pixels overtime
			if(selectedCloud.allPixels.length <= selectedCloud.maxNumberofPixels){
				for(var j = 0; j < selectedCloud.rainStrength; j++){
					selectedCloud.addPixel(new Raindrop(2, 4, selectedCloud.getRandomLocation(), new Vector2d(0,7), new Vector2d(0,0.05), new Color(0,0,128,1)));
				}
			}
		}
	}
	// TODO: Create object for mouse
	function getMousePos(canvas, evt) {
        var rect = canvas.getBoundingClientRect();
        return new Vector2d(evt.clientX - rect.left, evt.clientY - rect.top);
    }
});

// Basic objects
class Sprite{
	constructor(width, height){
		this.width = width;
		this.height = height;
		this.color = new Color(0,0,0,0);
	}
	setColor(color){
		this.color = color;
	}
}

class Pixel extends Sprite{
	constructor(widht, height, location, velocity, acceleration, color){
		super(widht, height);
		this.location = location;
		this.velocity = velocity;
		this.acceleration = acceleration;
		this.alive = true;
		this.setColor(color);
	}
	update(){
        this.velocity.add(this.acceleration);
        //this.velocity.limit(topspeed);
        this.location.add(this.velocity);
	};
	setHeight(height){
        this.height = height;
    };
    setWidth(width){
        this.width = width;
    }
}

class Spawner extends Sprite{
	constructor(width, height, location, maxNumberofPixels){
		super(width, height)
		this.allPixels = [];
		this.location = location;
		this.maxNumberofPixels = maxNumberofPixels;
	}
	addPixel(pixel){
		if(this.allPixels.length <= this.maxNumberofPixels)
			this.allPixels.push(pixel);
	}
	removePixels(deadPixelContainer){
		for(var i = 0; i < deadPixelContainer.length; i++){
			try{
				var pixelContainer = this.allPixels.slice();
				pixelContainer.splice(this.allPixels.findIndex(v => v === deadPixelContainer[i]), 1).slice();
				this.allPixels = pixelContainer.slice();
				//console.log("Cleaning...");
			}catch(e){
				console.log(e);
			}
		}
	}
	hover(mousePosition){
		if(mousePosition.x > this.location.x 
		&& mousePosition.x < this.location.x + this.width
		&& mousePosition.y > this.location.y
		&& mousePosition.y < this.location.y + this.height)
			return true;
		return false;
	}
}


// Custom objects
class Cloud extends Spawner{
	constructor(width, height, location, maxNumberofPixels, rainStrength){
		super(width, height, location, maxNumberofPixels);
		this.isClicked = false;
		this.rainStrength = rainStrength;		// how often cloud spawns new pixels per update cycle.
	}
	getRandomLocation(){
		var minWidth = this.location.x;
		var maxWidth = this.location.x + this.width;
		var minHeight = this.location.y + this.height/2; // don't count upper part of cloud. Rain forms at the bottom.
		var maxHeight = this.location.y + this.height;
		var randomWidthLocation = Math.random() * (maxWidth - minWidth + 1)+minWidth;
		var randomHeightLocation = Math.random() * (maxHeight - minHeight + 1) + minHeight;
		return new Vector2d(randomWidthLocation, randomHeightLocation); 
	}
}

class Background extends Sprite{
	constructor(width, height, color){
		super(width, height);
		this.isPaused = false;
		this.allClouds = [];
		this.pixelCount = 150;
		this.setColor(color);
	}
	addCloud(cloud){
		this.allClouds.push(cloud);
	};
    refreshCanvas(){
        this.width = $(window).width();
        this.height = $(window).height();
    };
    pause(){
        this.isPaused = true;
    };
    start(){
        this.isPaused = false;
    }
}

class Raindrop extends Pixel{
	constructor(widht, height, location, velocity, acceleration, color){
		super(widht, height, location, velocity, acceleration, color);
		
	}
	checkEdges(background) {
        if (this.location.y > background.height) {
            this.alive = false;
			
        }
    };
}

// Standard objects
class Color{
	constructor(r,g,b,o){
		this.red = r;
		this.green = g;
		this.blue = b;
		this.opacity = o;
	}
	getColorString(){
        return "rgba("+this.red+","+this.green+","+this.blue+","+this.opacity+")";
    }
}

class Vector2d{
	constructor(x, y){
		this.x = x;
		this.y = y;
	}
	add(vector2d) {
        this.x += vector2d.x;
        this.y += vector2d.y;
    };
    sub(vector2d) {
        this.x -= vector2d.x;
        this.y -= vector2d.y;
    };
    mult(mult) {
        this.x *= mult;
        this.y *= mult;
    };
    div(div) {
        this.x /= div;
        this.y /= div;
    };
    mag() {
        return Math.sqrt(this.x * this.x, this.y * this.y);
    };
    norm() {
        var m = mag();
        if (m !== 0) {
            div(m);
        }
    }
}
</script>
</body>
</html>

Paste in document and save as .html

Hope you like it.


#2

Oh my word… that is honestly amazing. I cannot you took the time to make this great masterpiece.


#3

This may be just me, but I don’t think of opacity in a color, so much as alpha-channel, which is what the a stands for in rgba.

What’s more, I would prefer the names in the parameter list, and use simple instance variable names.

class Color{
  constructor (red, green, blue, alpha) {
    this.r = red;
    this.g = green;
    this.b = blue;
    this.a = alpha;
  }
  getColorString () {
    return "rgba("+this.r+","+this.g+","+this.b+","+this.a+")";
  }
}

Further to that we have template literals in ES6 which take all the bother out of string interpolation.

  return `rgba(${this.r}, ${this.g}, ${this.b}, ${this.a})`

That’s me nit-picking again. Don’t let it get to you.

Did I say how much I liked this project (though I’ve yet to run it)? I’m reading the code like a novel and it speaks well of itself. If you give a second thought to what I’ve just said, perhaps I’ll run the revision.


#4

Basic concept costed me about 4-6 hours. I dont plan out what i make when i start (originally it whas suposed to be a rain shower with an umbrella). Making it nice and work correctly is the other 30 hours ish. Glad you liked it.


#5

Makes sence. Every time i googled on this they call it opacity, so i took the o. Never realy came to mind its a alpha channel.

Ill use this. Its a pain to plant the +'s and stuff on the right place. Have to convert pretty much all of the var’s to let’s anyway. might as well go full es6. A little of a annoyance tho that this kind of string interpolation cant use the normal' or " characters for it. It requires a `.

Aaah wel my good sir. This is a tale older than mankind itself. Afcourse it has to be good. Why else would it excist this long, right ?


#6

We get used to it with use. The nice thing about the new syntax is we can use it on all strings, not just for interpolation. I suspect that the working group saw it as a backward compatibility issue to mess around with the standard quotes.