Skip welcome & menu and move to editor
Welcome to JS Bin
Load cached copy from
 
<!DOCTYPE html>
<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 x
public
Bin info
anonymouspro
0viewers