2
\$\begingroup\$

A combination of various css transitions (background-color, box-shadow, opacity, transform) and getComputedStyle method of window object made up an effect resembling dust scattering that takes on the color of the dusty element.

<html>
<head>
<style>
.centered { 
    bottom: 0; 
    left: 0; 
    margin: auto; 
    position: absolute; 
    right: 0; 
    top: 0; 
}
.landscape-size { max-height: 480px; max-width: 640px; min-height: 240px; min-width: 320px; }
.content-drum { }
.dot { 
    background-color: #dfdfdf; 
    border-radius: 50% 50% 50% 50%;
    cursor: pointer; 
    height: 30px; 
    position: absolute; 
    width: 30px; 
}
.left-lane { background-color: transparent; left: 0; height: 100%; position: absolute; width: 80px; }
.whirl { background-color: lightslategrey; border-radius: 50%; cursor: pointer; height: 40px; transition: background-color 1s linear, box-shadow 1s linear; width: 40px; }

.yellow { background-color: yellow; }
.yellow.light-on { box-shadow: -5px 5px 20px 5px yellow; }
.lightblue { background-color: lightblue; }
.lightblue.light-on { box-shadow: -5px 5px 20px 5px lightblue; }
.lightsalmon { background-color: lightsalmon; }
.lightsalmon.light-on { box-shadow: -5px 5px 20px 5px lightsalmon; }
.salmon { background-color: salmon; }
.salmon.light-on { box-shadow: -5px 5px 20px 5px salmon; }
.orangered { background-color: orangered; }
.orangered.light-on { box-shadow: -5px 5px 20px 5px orangered; }
.mediumpurple { background-color: mediumpurple; }
.mediumpurple.light-on { box-shadow: -5px 5px 20px 5px mediumpurple; }
.turquoise { background-color: turquoise; }
.turquoise.light-on { box-shadow: -5px 5px 20px 5px turquoise; }
.teal { background-color: teal; }
.teal.light-on { box-shadow: -5px 5px 20px 5px teal; }
.slategrey { background-color: slategrey; }
.slategrey.light-on { box-shadow: -5px 5px 20px 5px slategrey; }

/** particle css */
.particle {
    border-radius: 50% 50% 50% 50%;
    position: absolute;
    transition: opacity 1s linear;
}
.transparent-particle {
    box-shadow: none;
    opacity: 0;
}
.opaque-particle {
    opacity: 1;
}
.drifter {
    transition: box-shadow 1s linear, opacity 1s linear, transform 2s linear;
}
.haze {
    height: 2px;
    width: 2px;
}
.dew {
    height: 3px;
    width: 3px;
}
.powder {
    height: 4px;
    width: 4px;
}
/** end particle css */
</style>

</head>

<body onload="layout()">
    <div class="content-drum centered landscape-size"></div>
</body>

<script>
var emptyEffect = { on: () => { }, off: () => { } }
function computeCoordinates(numberOfRadians) {
    var steps = [];
    var radians = ( (360 / numberOfRadians) * 2 * Math.PI ) / 360;
    var cos = Math.cos(radians);
    var sin = Math.sin(radians);
    var contentDrum = document.querySelector(".content-drum");
    var radius = Math.round( (contentDrum.offsetHeight * 0.6) / 2);
    var originX = Math.round( contentDrum.offsetWidth / 2);
    var originY = Math.round( contentDrum.offsetHeight / 2);

    var x1 = radius - 1;
    var y1 = radius - 1;
    for ( var i = 0; i < numberOfRadians; i++ ) {
        var x2 = (Math.round( x1 * cos - y1 * sin ));
        var y2 = (Math.round( y1 * cos + x1 * sin ));
        
        var x = originX + x2;
        var y = originY + y2;
        
        steps.push([x, y]);
        
        x1 = x2;
        y1 = y2;
    }
    
    return steps;
}
function createDiv(colorCssClass) {
    var dot = document.createElement("div");
    dot.classList.add("dot");
    dot.classList.add(colorCssClass);
    // dot.addEventListener("click", function() { animate(this); });
    return dot;
}
ZipperEffect.getLightableSource = function(elementSource) {
    if ( elementSource == null) {
        return () => { var lightables = Array.from(document.querySelectorAll(".dot"));
                        return lightables; 
                      };
    }
    return () => { return elementSource().getDots(); }
}
ZipperEffect.supply = function(elementSource, paceMillis, numberOfRounds, stepEffect) {
    var zipperEffect = new ZipperEffect(paceMillis, numberOfRounds, stepEffect);
    zipperEffect.lightableSource = ZipperEffect.getLightableSource(elementSource);
    return zipperEffect;
}
function ZipperEffect(paceMillis, numberOfRounds, stepEffect) {
    this.lightableSource = () => { return []; };
    this.onings = [];
    this.offings = false;
    this.paceMillis = paceMillis;
    this.numberOfRounds = numberOfRounds || 1;
    this.stepEffect = stepEffect || emptyEffect;
}
ZipperEffect.prototype.on = function(startingWith) {
    if ( this.offing ) { return; }
    var rounds = 0;
    var timeoutPerRound = this.lightableSource().length * this.paceMillis;
    while ( this.numberOfRounds > rounds ) {
        var ze = this;
        setTimeout(function() { ze.zipOn(startingWith); }, rounds * 1000 );
        rounds++;
    }
}
ZipperEffect.prototype.zipOn = function(startingWith) {
    if ( this.offing ) { return; }
    var lightables = this.lightableSource();
    var numberOfLightables = lightables.length;
    var startIndex = lightables.indexOf(startingWith);
    var lightedOn = 0;
    var i = startIndex;
    while ( lightedOn < numberOfLightables ) {
        
        var onTimeInMillis = (lightedOn + 1) * this.paceMillis;
        var offTimeInMillis = onTimeInMillis + ( 2 * (numberOfLightables - lightedOn) * this.paceMillis );
        this.onings.push( this.light( lightables[i], onTimeInMillis, offTimeInMillis ) );
        
        i = (i + 1) % numberOfLightables;
        lightedOn++;
    }
}
ZipperEffect.prototype.off = function() {
    this.offing = true;
    var toCancel = this.onings;
    this.onings = [];
    this.offing = false;
    toCancel.forEach( (oning) => { clearTimeout(oning[0]); clearTimeout(oning[1]); } );
    this.lightableSource().forEach( (detail) => { detail.classList.remove("light-on"); } );
}
ZipperEffect.prototype.light = function(lightable, millisecondsUntilOn, millisecondsUntilOff) {
    var scheduledOnId = setTimeout(() => { lightable.classList.add("light-on"); 
                                           this.stepEffect.on(lightable); }
                                  , millisecondsUntilOn);
    var scheduledOffId = setTimeout(() => { lightable.classList.remove("light-on"); } , millisecondsUntilOff );
    return [ scheduledOnId, scheduledOffId ];
}


function DustEffect(dusted) {
    this.dusted = dusted;
}
DustEffect.supply = function(dusted) {
    if (dusted == null ) { return emptyEffect; }
    return new DustEffect(dusted);
}
DustEffect.prototype.on = function(duster) {
    var cleanUpDustCssClassName = "cleanUp" + Math.round(Math.random() * 100);
    var powder = this.createParticles(20, "powder " + cleanUpDustCssClassName);
    var dew = this.createParticles(20, "dew " + cleanUpDustCssClassName);
    var haze = this.createParticles(40, "haze " + cleanUpDustCssClassName);
    var particles = powder.concat(dew).concat(haze);
    var particles = shuffle(particles);
    var spreadRadius = 10;
    var round = 1;    
    while ( particles.length > 0 ) {
        this.spreadParticles(duster, this.dusted, particles.splice(0, 20), spreadRadius + round * 10, cleanUpDustCssClassName);
        round++;
    }
}
DustEffect.prototype.off = function() { }
DustEffect.prototype.light = function() { }
DustEffect.prototype.scheduleCleanUp = function(cssClassNameToCleanUp) {
    var cleanUpAfterMillis = 2500;
    setTimeout(() => {
        var particles = document.querySelectorAll(".particle." + cssClassNameToCleanUp);
        particles.forEach( (particle) => {particle.remove(); } );
    }, cleanUpAfterMillis);
}
DustEffect.prototype.spreadParticles = function (duster, dusted, particles, spreadRadius, cleanUpDustCssClassName) {
    var colorOffAfterMillis = 1000;
    var radians = ( (360 / particles.length) * 2 * Math.PI ) / 360;
    var cos = Math.cos(radians);
    var sin = Math.sin(radians);
    var originX = Math.round(duster.offsetLeft + duster.offsetWidth / 2);
    var originY = Math.round(duster.offsetTop + duster.offsetHeight / 2);

    var backgroundColor = window.getComputedStyle(duster, null).getPropertyValue("background-color");
    var radius = spreadRadius || 20;
    var x1 = radius;
    var y1 = radius;
    for ( var i = 0; i < particles.length; i++ ) {
        var variation = Math.ceil(Math.random() * (radius / 4) );
        var x2 = Math.round( (x1 + variation) * cos - (y1 + variation) * sin );
        var y2 = Math.round( (y1 + variation) * cos + (x1 + variation) * sin );
        
        var x = originX + x2 + variation;
        var y = originY + y2 + variation;
        particles[i].style.left = x;
        particles[i].style.top = y;
        particles[i].style.boxShadow = "0px 0px 8px 0px " + backgroundColor;
        particles[i].style.backgroundColor = backgroundColor;
        
        dusted.appendChild(particles[i]);
        this.colorOnOff(particles[i], colorOffAfterMillis);
        this.driftOn(particles[i], x2, y2);
        
        x1 = x2;
        y1 = y2;
    }
    this.scheduleCleanUp(cleanUpDustCssClassName);
}
DustEffect.prototype.colorOnOff = function(toColor, colorOffAfterMillis) {
    setTimeout(() => {
        toColor.classList.add("opaque-particle");
        toColor.classList.remove("transparent-particle");
    }, 1); 
    setTimeout(() => {
        toColor.classList.add("transparent-particle");
        toColor.classList.remove("opaque-particle");
    }, (colorOffAfterMillis || 1000));
}
DustEffect.prototype.driftOn = function(drifter, x, y) {
    setTimeout(() => {
        drifter.classList.add("drifter");
        drifter.style.transform = "translate(" + x + "px, " + y + "px)";
    }, 10); 
}
DustEffect.prototype.createParticles = function(numberOfParticles, cssClass) {
    var particles = [];
    for ( var i = 0; i < numberOfParticles; i++ ) {
        var particle = document.createElement("div");
        particle.className = cssClass;
        var cssClassList = particle.classList; 
        cssClassList.add("particle");
        cssClassList.add("transparent-particle");
        particles.push(particle);
    }
    return particles;
}
function shuffle(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 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 waver = (function() {
    return { between: function(min, max) {
                 return Math.floor( Math.random() * ( max - min + 1 ) + min );
           }
           , until: function(max) {
                 return Math.floor( Math.random() * ( max + 1 ) );
           }
};})();
var dusterEffect = DustEffect.supply(document.querySelector(".content-drum"));
var zipperEffect = ZipperEffect.supply(null, 100, 1, dusterEffect);
var colors = ["yellow", "lightsalmon", "lightblue", "salmon", "teal", "mediumpurple", "turquoise", "orangered", "slategrey", "lightsalmon", "turquoise"];
function layout() {
    var coordinates = computeCoordinates(colors.length);
    var contentDrum = document.querySelector(".content-drum");
    for ( var i = 0; i < coordinates.length; i++ ) {
        var dot = createDiv(colors[i]);;
        dot.style.left = coordinates[coordinates.length - 1 - i][0];
        dot.style.top = coordinates[coordinates.length - 1 - i][1];
        contentDrum.appendChild(dot);
    }
    var animateMillis = 4000;
    var dots = document.querySelectorAll(".dot");
    var undust = vainer.supply()
                       .schedule(() => { animate(dots[waver.between(0, dots.length - 1)]); return animateMillis; })
                       .schedule(() => { animate(dots[waver.between(0, dots.length - 1)]); return animateMillis; })
                       .schedule(() => { animate(dots[waver.between(0, dots.length - 1)]); return animateMillis; })
                       .schedule(() => { animate(dots[waver.between(0, dots.length - 1)]); return animateMillis; });
    
    var animator = () => {
        var delayMillis = undust.shift();
        if ( delayMillis == false ) { return; }
        setTimeout(() => { animator(); }, delayMillis);
    };
    var delayMillis = 1000;
    setTimeout(() => { animator(); }, delayMillis);
}

function animate(startingWith) { zipperEffect.on(startingWith); }
</script>
</html>

Is there a plainer implementation or a formal wide accepted alternative implementation? Any other feedback is welcome without a doubt.

\$\endgroup\$
0

0

Your Answer

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