1
\$\begingroup\$

Developed around visibility CSS property and HTMLElement#offsetHeight and HTMLElement#offsetWidth properties places HTMLElements generated by an HTMLElement source in columns layout attempting to achieve the minimum height of the columns by placing each element on the less lengthy column at the time of placing the element.

Main components are sprinkler with a sieve, that is a collection of column objects, supporting two modes (puddle and sprite) and a source of HTMLElements. The elements to be placed are one at the time retrieved from the source then passed to the sieve that pass it to the less lengthy column that stores it.

<html>
<head>
<style>
body { background-color: #cdcecd; }
.centered { 
    bottom: 0; 
    left: 0; 
    margin: auto; 
    position: absolute; 
    right: 0; 
    top: 0; 
}
.drop {
    position: absolute;
    background-blend-mode: overlay;
    background-position: center;
    background-repeat: no-repeat;
    background-size: contain;
    border-radius: 50%;
    height: auto;
    opacity: .5;
    width: 5%;
}
.airly {
    background-color: #fefefe;
    height: 25px;
    width: 25px;
}
.vapourly {
    background-color: #dcdcdc;
    height: 50px;
    width: 50px;
}
.dense {
    background-color: #c0c0c0;
    height: 100px;
    width: 100px;
}
.drop.loading { visibility: hidden; }
.sprinkled {
    background-color: #eeeeee;
    height: 90%;
    width: 90%;
}
</style>
<script type="text/javascript">
var sprinkler = function(sprinkled) {

    var coordinates = (function() {

        var toNumber = (string) => {
            var digits = Array.from(string || "")
                              .filter( (character) => { return !isNaN(character) || "." == character; } )
                              .join("");
            return ( digits.length == 0 ) ? 0 : parseInt(digits);
        };

        return { of : (htmlElement) => {
                     var element = htmlElement || { style : { top : "", left : "" } };
                     return { x : toNumber(element.style.left)
                            , y : toNumber(element.style.top)
    };}};})();

    var peeker = function(enabler) {
          var peeked = void(0);
          var availabilities = { content : (toPeek) => {
                     if ( enabler(peeked, toPeek) ) {
                         peeked = toPeek;
                     }
                     return availabilities;
                 }
                 , result : () => { return peeked; }
               };

          return availabilities;
    };

    var vainer = (function() {
        return { supply: (purpose) => {
            var vainers = [() => { return false; }];
            if ( purpose ) { vainers.unshift(() => { vainers.shift(); return purpose(); }); }
            return { shift: function() { return vainers[0](); }
                   , schedule: function(schedulee) {
                       vainers.unshift( () => { vainers.shift(); return schedulee(); } );
                       return this;
                   }
            };
        }};
    })();

    var sieve = (function() {

        var mode = (function() {
            var heightOffset = (element) => { return (element || {offsetHeight: 0}).offsetHeight };
            var plainOffset = (element) => { return 0; };
            var offset = heightOffset;

            var column = () => {

                var content = [];

                var abacus = (function() {
                    var sum = 0;
                    return { count : (something) => { sum += something; }
                           , counted : () => { return sum; }
                           , reset : () => { sum = 0; } };
                })();

                var height = () => {
                    abacus.reset();
                    content.forEach( (element) => { abacus.count(element.offsetHeight || 0); } );
                    return abacus.counted();
                };

                return { add : (element) => { content.push(element); }
                       , height : () => { return height(); }
                       , coordinates : () => { return { x : coordinates.of(content[0]).x
                                                      , y : height() + offset(content[0]) } }
                };
            };

            return { puddle : () => { offset = heightOffset; return column; }
                   , sprite : () => { offset = plainOffset; return column; }
            };
        })();

        var puddleColumn = mode.puddle();
        var column = puddleColumn;

        var maxify = peeker( (previous, column) => { return column.height() >= (previous || column).height(); } );

        var columns = [];
        var minimumHeight = 0;

        var shell = { add : { column : () => { columns.push(column()); return shell; }
                            , content: (toAdd) => {

                                  if ( columns.length == 0 ) { columns.push(column()); }

                                  var column = columns[0];

                                  var minify = peeker( (previous, column) => { return column.height() <= (previous || column).height(); } );
                                  columns.forEach( (column) => { minify.content(column); } );
                                  minify.result().add(toAdd);

                                  return { x : minify.result().coordinates().x
                                         , y : minify.result().coordinates().y - toAdd.offsetHeight};
                             }
                    }
                    , length : () => { return columns.length; } };

        return { puddle: () => { column = puddleColumn; return shell; }
               , sprite: () => { column = mode.sprite(); return shell; }
    };})().puddle();

     var enabler = (function(purpose) {

        var enablers = [ purpose, () => {} ];

        return { enable : () => { return enablers[0]; }
               , shift : () => { if ( enablers.length > 1 ) { enablers.shift(); } }
        };
    })( (after) => { sieve.add.column(); after(); } );

    var load = (function() {
        var loadingDelayMillis = 200;
        return { progressive: (multiplier, toLoad) => { setTimeout( () => { toLoad.classList.remove("loading"); }, multiplier * loadingDelayMillis); }
               , flashback: (multiplier, toLoad) => { toLoad.classList.remove("loading"); }
        };
    })();

    return function(source) {

        var sprinkledCoordinates = sprinkled.getBoundingClientRect() || { x : 0, y : 0, left : 0, top : 0 };
        var origin = (function(originX, originY) {
            var x = originY;
            var y = originX;

            return { widthed : (gap) => { x += parseFloat(gap); return x; }
                   , heighted : (gap) => { y+= parseFloat(gap); return y; }
                   , recorded : () => { return { x: x, y: y}; }
                   , reset : { height: () => { y = originY; }
                             , width: () => { x = originX; }
                             , both: () => { x = originX; y = originY; } }
        };})(sprinkledCoordinates.x, sprinkledCoordinates.y);

        var viewport = { height: sprinkled.offsetHeight - sprinkled.offsetHeight * 0.1
                       , width: sprinkled.offsetWidth - sprinkled.offsetWidth * 0.1 };
        var i = 0;
        while ( source.callworthy() ) {
            var drop = source.next();
            drop.classList.add("drop");
            drop.classList.add("loading");
            drop.style.left = origin.recorded().x;
            drop.style.top = origin.recorded().y;

            sprinkled.appendChild(drop);

            if ( origin.recorded().x > viewport.width ) { enabler.shift(); }

            enabler.enable()( () => { origin.widthed(drop.offsetWidth); } );

            var position = sieve.add.content(drop);
            drop.style.left = position.x + "px";
            drop.style.top = position.y + "px";

            load.progressive(i++, drop);
        }
    }
};
</script>
</head>
<body>
<div id="sprinkled" class="sprinkled centered"></div>
<script type="text/javascript">
var spread = (function() {

    var source = (function() {

        var counter = (function(from) {
            var tapCounter = from;
            return { incremented : () => { return ++tapCounter; }
                   , decremented : () => { return --tapCounter; }
                   , counted : () => { return tapCounter; }
                   , reset : () => { tapCounter = from; }
                   };
        })(0);
        var drop = (className) => {
            var div = document.createElement("div");
            div.className = className;
            return div;
        };
        var condense = (className) => {
            var drops = [];
            for (var i = 0; i < 53; i++) {
                drops.push(className);
            }
            return drops;
        };
        var shuffle = function(array) {
            var shuffledArray = [];
            while ( array.length > 0 ) {
                var index = Math.floor(Math.random() * array.length);
                shuffledArray = shuffledArray.concat( array.splice(index, 1) );
            }
            return shuffledArray;
        };

        var drops = shuffle(condense("airly").concat(condense("vapourly")).concat(condense("dense")));

        return { callworthy: () => { return counter.counted() < drops.length; }
               , next: () => { return drop(drops[counter.incremented()]); }
        };
    })();

    sprinkler(document.getElementById("sprinkled"))(source);
})();
</script>
</body>
</html>

Among interests: improvements of javascript code, alternatives to implement sprinkler behaviour, existing alternatives already available or anything else that could be an improvement.

\$\endgroup\$
2
  • 2
    \$\begingroup\$ Would you please add a JS snippet, so stackexchange contributors can easily click it to run and view the effect? Or failing that, please offer a link to some site where the effect is running and visible. \$\endgroup\$
    – J_H
    Commented May 18 at 0:59
  • 1
    \$\begingroup\$ @J_H now that an answer has been posted it may be more difficult to get a snippet added without risk of answer invalidation. I did create this fiddle for it though \$\endgroup\$ Commented May 19 at 23:12

1 Answer 1

3
\$\begingroup\$

Partial review

Just looking at part of your code (the shuffle caught my eye) because there is way too much code to workout what it does just by looking at it (your description is too vague).

I will assume the code runs as intended and there are not bugs.

Avoid redundant and unused code.

Starting at the line var spread = (function() { you are assigning the return of the Immediately Invoked Function Expression (IIFE) to spread. However the function returns nothing (undefined) hence spread === undefined. spread is unused and should not be there.

Use language shortcuts

Always use the shortest form available to the language unless you wish to illustrate a particular intent (which I do not see in your code).

Arrow functions

Use arrow functions rather than function expressions. var source = (function() { is better as const source = (() => {

Object shorthand

Object properties that are function can use object function shorthand.

example

return { 
    incremented() { return ++tapCounter }, 
    decremented() { return --tapCounter },
    counted() { return tapCounter },
    reset() { tapCounter = from },
};

Only what is needed

The object assigned to counter has two unused functions decremented and reset.

My guess is that you added them to either fill out a future need, or complete the set? Don't add unused code, only what is needed and nothing else.

Because you have the unused function reset there is no need to store the base value making the object even simpler.

Really... for a simple counter the source code complexity is not needed (see rewrite).

Use constants

When a variable does not change define it as a constant const varName = varContent;, it protect you from accidentally changing a value that should not change.

Function scope

Variables declared with var are function scoped. As such you should declare them where the JS engine declares them, at the top of the function.

It's called hoisting. Always hoist your function scoped variables. Else define block scoped variables using let or const

Avoid thrashing memory and needless iterations

Looking at your shuffle function. It is horrifically inefficient. Though on a modern machine clients will not notice, you should still aim for efficient code as its your client's power your code consumes, and hence their money (and save the planet please!)

When you use Array.concat the JS engine creates a new array and copies the content of the two arrays into the new one. The cost of allocating the memory and then iterating the two arrays can be avoided if you take care.

When you splice an array item using Array.splice. All the items above the splice item must be moved to accommodate the new or removed items.

The shuffle algorithm can be efficient with a linear complexity \$O(n)\$. However your implementation is quadratic \$O(n^2)\$.

If we assume that GC will not trigger until code exits (most likely) storage is a terrible quasilinear \$O(n log(n))\$ rather than the nice constat \$O(1)\$ it could be.

You use a concat and splice where there should be just a simple swap of two items.

Improved array functions

A Shuffle array inplace that swaps random items.

// shuffles array inplace. complexity O(n) storage O(1)
const shuffle = array => {
    var idx = array.length;
    while (idx-- > 0) {
        const rIdx = Math.random() * idx | 0;  // | 0 floors to Uint32 
        [array[rIdx], array[idx]] = [array[idx], array[rIdx]];   // swap
    }
    return array;
};

Where you create the drops array can also avoid the contacts by letting the condense function take an array to add to.

const condense = (name, array = [], count = 53) => {
    while (count-- > 0) { array.push(name); }
    return array;
};

const drops = shuffle(condense("airly", condense("vapourly", condense("dense"))));

Rewrite

A quick rewrite of the last script tag using points outlined above and some addition changes.

Should be functionally identical but considerably less resource hungry.

I removed counter altogether as it seems overly complex for a simple counter. Rather rewrite uses variable count

The rewrite could be simplified (source code) but I made a small attempt to keep the code similar.

const byId = (idStr, root = document) => root.querySelector("#" + idStr);
const Tag = (name, props = {}) => Object.assign(document.createElement(name), props);
(() => {
    const SPRINKLE_ID = "sprinkled";
    const CLASS_NAMES = ["airly", "vapourly", "dense"];
    const CLASS_COUNTS = 53;
    const DROP_TAG = "div";

    sprinkler(byId(SPRINKLE_ID))((() => {
        var count = 0; /* << replaces counter object */
        const drop = className => Tag(DROP_TAG, {className});
        const condense = (name, array = [], count = CLASS_COUNTS) => {
            while (count-- > 0) { array.push(name) }
            return array;
        };
        const shuffle = array => {
            var idx = array.length;
            while (idx-- > 0) {
                const rIdx = Math.random() * idx | 0;  
                [array[rIdx], array[idx]] = [array[idx], array[rIdx]];
            }
            return array;
        };
        const drops = shuffle(condense(CLASS_NAMES[0], condense(CLASS_NAMES[1], condense(CLASS_NAMES[2]))));
        return { 
            callworthy() { return count < drops.length },
            next() { return drop(drops[++count]) },
        };
    })());
})();
  • Note: Above code is untested and as such may contain typos or bugs.
  • Note: Original code function next never returns the first item in the array drops. Not sure if this is intentional so rewrite does same and provides 158 of 159 (53 * 3) items.
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.