<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<script src="https://code.jquery.com/jquery-3.1.0.js"></script>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/konva/3.2.4/konva.min.js" integrity="sha256-P+/oNcF4xLPruZZeyVUFrTGosjyJRHxa9zn7j6oeRG8=" crossorigin="anonymous"></script>
<title>JS Bin</title>
</head>
<body>
<div id="container">
<div class="hero">X</div>
</div>
</body>
</html>
@import url('https://2019.budejovickymajales.cz/assets/css/weather.css');
html, body, div#container {
width: 100%;
height: 100%;
margin: 0;
}
div.hero {
font-size: 20rem;
font-weight: bold;
position: absolute;
top: 0;
left: 0;
width: 100%;
z-index: 2;
text-align: center;
}
var MajalesWeather = (function() {
'use strict';
/**
* @param {HTMLElement} container
* @constructor
*/
var Weather = function(container) {
/**
* @type {HTMLDivElement}
*/
this.container = document.createElement('div');
/**
* @type {{foreground: {Konva.Stage}, background: {Konva.Stage}}}
*/
this.stages = {'foreground': null, 'background': null};
/**
* Foreground and background layers
* @type {{foreground: {Konva.BaseLayer}, background: {Konva.BaseLayer}}}
*/
this.layers = {'foreground': null, 'background': null};
/**
* Values calculated in onResize()
* @type {Object}
*/
this.measures = {};
/**
* Current animation state
* @type {Object}
*/
this.state = {};
// container and layers initialization
this.container = container;
this.container.classList.add('majalesweather-container');
var foreground = document.createElement('div');
foreground.classList.add('majalesweather-layer', 'majalesweather-layer-foreground');
this.container.appendChild(foreground);
this.stages.foreground = new Konva.Stage({
container: foreground,
width: 1,
height: 1
});
this.layers.foreground = new Konva.FastLayer();
this.stages.foreground.add(this.layers.foreground);
var background = document.createElement('div');
background.classList.add('majalesweather-layer', 'majalesweather-layer-background');
this.container.appendChild(background);
this.container.appendChild(foreground);
this.stages.background = new Konva.Stage({
container: background,
width: 1,
height: 1
});
this.layers.background = new Konva.FastLayer();
this.stages.background.add(this.layers.background);
this.onResize(); // calculate initial values
// animation hook
var animation = new Konva.Animation(function(frame) {
var ms = 1000 / frame.frameRate;
var t = frame.timeDiff / ms;
t = 1;
return this.tick(Math.round(t));
}.bind(this), [this.layers.background, this.layers.foreground]);
animation.start();
$(window).resize(function() {
this.onResize();
}.bind(this));
};
/**
* Sets current weather state
* @param {String|null} weather state defined by one of the TYPE_* constants
*/
Weather.prototype.setWeather = function(weather) {
// resetting the current canvas
if(this.state['drops'] !== undefined) {
for(var d = 0; d < this.state['drops'].length; d++) {
this.state['drops'][d]['element'].destroy();
}
}
this.state = {};
for(var i = 0; i < Object.keys(this.layers); i++) {
this.layers[Object.keys(this.layers)[i]].clear();
this.layers[Object.keys(this.layers)[i]].destroyChildren();
}
this.container.classList.remove('majalesweather-raining-day', 'majalesweather-raining-night');
// initialization of the new weather state
if(weather === Weather.TYPE_RAINING_DAY || weather === Weather.TYPE_RAINING_NIGHT) {
this.state['config'] = {
'step': { // controls the speed of the animation (each tick the position moves by 0.0075)
'position': 0.0075,
'opacity': 0.1
},
'rotation': -25, // rainfall angle (in degrees)
'numDrops': 30 // number of raindrops
};
this.state['drops'] = []; // raindrops onstage
this.state['trash'] = []; // raindrops offstage, ready to be recycled when needed onstage
this.state['drop'] = {
'orig': new Konva.Path({ // raindrop
'x': 0,
'y': 0,
'data': 'M17.229.344,7.864,3.1A10.165,10.165,0,0,0,.921,15.434,9.953,9.953,0,1,0,16.083,4.47a1,1,0,0,1-.322-1.291Z',
'fill': '#730FFF',
'width': 21,
'height': 23,
'listening': false,
'perfectDrawEnabled': false
}),
'cache': { // we don't use the original raindrop but convert it to a raster image for performance reasons
'scale': null, // size of the exported image
'element': null, // exported image
'caching': false // is the original raindrop currently exporting to an image?
}
};
switch(weather) {
case Weather.TYPE_RAINING_DAY:
this.container.classList.add('majalesweather-raining-day');
break;
case Weather.TYPE_RAINING_NIGHT:
this.container.classList.add('majalesweather-raining-night');
break;
}
this.state['weather'] = weather;
}
};
/**
* Draws a frame
* @param {Number} tRef number of frames since the last draw
* @returns {boolean} has any change on cavas occured?
*/
Weather.prototype.tick = function(tRef) {
if(this.state['weather'] === undefined || this.state['weather'] === null) {
return false;
}
if(this.state['weather'] === Weather.TYPE_RAINING_DAY || this.state['weather'] === Weather.TYPE_RAINING_NIGHT) {
var dropScale = 6.25 * Math.min(this.measures['canvas']['width'], this.measures['canvas']['height']) / 720; // raindrop size
if((this.state['drop']['cache']['element'] === null || this.state['drop']['cache']['scale'] !== dropScale) && !this.state['drop']['cache']['caching']) { // there's no rasterized raindrop or the rasterized version is smaller than needed
// rasterization of the vector raindrop for better performance
this.state['drop']['cache']['caching'] = true;
this.state['drop']['orig'].toImage({
'x': 0,
'y': 0,
'width': this.state['drop']['orig'].width(),
'height': this.state['drop']['orig'].height(),
'pixelRatio': dropScale, // raster in the desired size
'callback': function(img) {
var width = Math.ceil(this.state['drop']['orig'].width() * dropScale);
var height = Math.ceil(this.state['drop']['orig'].height() * dropScale);
this.state['drop']['cache']['element'] = new Konva.Image({
'image': img,
'x': 0,
'y': 0,
'width': width,
'height': height,
'offset': { // use the raindrop's center as its origin
'x': width / 2,
'y': height / 2
}
});
this.state['drop']['cache']['scale'] = dropScale;
for(var k = 0; k < this.state['trash'].length; k++) { // invalidate old offstage raindrops
this.state['trash'][k].destroy();
}
this.state['trash'] = [];
this.state['drop']['cache']['caching'] = false;
}.bind(this)
});
}
if(this.state['drop']['cache']['element'] === null) {
return false;
}
for(var i = 0; i < this.state['config']['numDrops'] - this.state['drops'].length; i++) { // create raindrops
var drop;
if(this.state['trash'].length > 0) { // recyclable raindrops are available, reuse them to avoid creating new nodes
drop = this.state['trash'][0];
this.state['trash'].splice(0, 1);
} else { // no recyclable raindrops, create a new one
drop = this.state['drop']['cache']['element'].clone();
}
if(Math.random() > 0.5) {
this.layers.foreground.add(drop);
} else {
this.layers.background.add(drop);
}
this.state['drops'].push({
'element': drop,
't': { // animation state (a number that controls how much the animation has advanced)
'position': 0,
'opacity': 0
},
'origin': { // coordinates of the point where the raindrop first appeared
'x': null,
'y': null
},
'scale': dropScale // raindrop size
});
}
for(var j = 0; j < this.state['drops'].length; j++) { // raindrop animation
if(this.state['drops'][j]['t']['position'] === 0) { // the raindrop has been newly put on stage, initialize its origin
this.state['drops'][j]['element'].opacity(1); // reset the opacity so even recycled raindrops (which fade out) are visible
this.state['drops'][j]['origin']['x'] = Weather.random(0.1, 1.5); // randomly position the raindrop on the stage
this.state['drops'][j]['origin']['y'] = Weather.random(-1, -0.2);
}
var y = this.state['drops'][j]['t']['position'] * this.state['config']['step']['position'] + this.state['drops'][j]['origin']['y'];
var x = Math.sin(this.state['config']['rotation'] * (Math.PI / 180)) * ((y - this.state['drops'][j]['origin']['y']) / Math.sin((90 - this.state['config']['rotation']) * (Math.PI / 180))) + this.state['drops'][j]['origin']['x']; // law of sines to make the raindrop fall under the defined angle
this.state['drops'][j]['element'].position({
'x': x * this.measures['canvas']['width'], // coordinates are defined as percentages (range 0-1), so we need to convert them to pixel values
'y': y * this.measures['canvas']['height']
});
if(y > 1 || x < 0) { // the raindrop has moved offscreen
if(this.state['drops'][j]['t']['opacity'] * this.state['config']['step']['opacity'] >= 1) { // it has faded away
this.state['drops'][j]['element'].remove(); // odstraníme kapku z obrazovky
if(!this.state['drop']['cache']['caching'] && this.state['drops'][j]['scale'] === dropScale) {
this.state['trash'].push(this.state['drops'][j]['element']); // save the raindrop so that it can be recycled
} else {
this.state['drops'][j]['element'].destroy(); // the raindrop is old and was created with different size than is now needed, let's throw it away
}
this.state['drops'].splice(j, 1);
} else { // we'll start fading the raindrop
this.state['drops'][j]['element'].opacity(1 - this.state['config']['step']['opacity'] * this.state['drops'][j]['t']['opacity']);
this.state['drops'][j]['t']['opacity'] += tRef;
}
}
if(this.state['drops'][j] !== undefined) { // make sure we didn't throw the raindrop away
this.state['drops'][j]['t']['position'] += tRef; // advance the animation
}
}
}
return true;
};
/**
* Calculates the canvas dimensions.
*
* This method calculates the necessary canvas dimensions based on the dimensions of the container and resizes the canvas.
*/
Weather.prototype.onResize = function() {
this.measures['canvas'] = {'width': this.container.clientWidth, 'height': this.container.clientHeight};
for(var i = 0; i < Object.keys(this.stages).length; i++) {
this.stages[Object.keys(this.stages)[i]].size({'width': this.measures['canvas']['width'], 'height': this.measures['canvas']['height']});
}
};
/**
* Generates a random number within the specified range.
* @param {Number} min
* @param {Number} max
* @returns {Number}
*/
Weather.random = function(min, max) {
return Math.random() * (max - min) + min;
};
Weather.TYPE_RAINING_DAY = 'raining-day';
Weather.TYPE_RAINING_NIGHT = 'raining-night';
return Weather;
})();
// script invocation
var weather = new MajalesWeather(document.getElementById('container'));
weather.setWeather(MajalesWeather.TYPE_RAINING_DAY);
Output
This bin was created anonymously and its free preview time has expired (learn why). — Get a free unrestricted account
Dismiss xKeyboard Shortcuts
Shortcut | Action |
---|---|
ctrl + [num] | Toggle nth panel |
ctrl + 0 | Close focused panel |
ctrl + enter | Re-render output. If console visible: run JS in console |
Ctrl + l | Clear the console |
ctrl + / | Toggle comment on selected lines |
ctrl + ] | Indents selected lines |
ctrl + [ | Unindents selected lines |
tab | Code complete & Emmet expand |
ctrl + shift + L | Beautify code in active panel |
ctrl + s | Save & lock current Bin from further changes |
ctrl + shift + s | Open the share options |
ctrl + y | Archive Bin |
Complete list of JS Bin shortcuts |
JS Bin URLs
URL | Action |
---|---|
/ | Show the full rendered output. This content will update in real time as it's updated from the /edit url. |
/edit | Edit the current bin |
/watch | Follow a Code Casting session |
/embed | Create an embeddable version of the bin |
/latest | Load the very latest bin (/latest goes in place of the revision) |
/[username]/last | View the last edited bin for this user |
/[username]/last/edit | Edit the last edited bin for this user |
/[username]/last/watch | Follow the Code Casting session for the latest bin for this user |
/quiet | Remove analytics and edit button from rendered output |
.js | Load only the JavaScript for a bin |
.css | Load only the CSS for a bin |
Except for username prefixed urls, the url may start with http://jsbin.com/abc and the url fragments can be added to the url to view it differently. |