In one of the projects customer asked to make a realistic animation of Christmas balls which to be started to swing being touched by the mouse. That’s what came out of it.
Move the mouse over the balls and they will begin to swing with a decreasing amplitude until they stop after a while.
JS code was not too easy to implement. Perhaps someone will find useful the ideas embedded in it.
When you hover over the ball, the swing time (from 3 to 7 seconds) and the swing amplitude in two planes are initialised – in the screen plane (maxAngle) and the perpendicular plane (maxScale).
var params = {
init: 0,
duration: 5000 + ( Math.random() * 2 - 1 ) * 2000,
maxScale: 1 + ( Math.random() * 2 - 1 ) * 0.25,
maxAngle: 10 * ( Math.random() * 2 - 1 )
};
The calculation of the position on the screen is made according to the formula of a damped mathematical pendulum:
function calcTransform( arrParams, percent ) {
var valS = 0;
var valR = 0;
var valO = 0;
var length = arrParams.length;
for ( var i = 0; i < length; i ++ ) {
var params = arrParams[i];
var time = ( params.init + percent ) * 100;
if ( time > 100 ) {
continue;
}
var cycles = 3;
var attenuation = 0.025;
var sin = Math.sin( time / 100 * 360 * Math.PI / 180 * cycles );
var amplitude = sin * Math.exp( - attenuation * time );
valS += amplitude * ( params.maxScale - 1 );
valR += amplitude * params.maxAngle;
var sin_90 = Math.sin( time / 100 * 360 * Math.PI / 180 * cycles - 90 * Math.PI / 180 );
valO = ( sin_90 + 1 ) / 2;
}
valO = valO / length;
valS = 1 + valS;
return {
transform: 'scale(' + valS + ') rotate(' + valR + 'deg)',
opacity: valO
};
}
Full html, css, js code is given below:
<style>
.ny-2018-wrapper {
position: relative;
}
.ny-2018-balls {
position: absolute;
right: 0;
top: 0;
height: 50vh;
width: 44vh;
z-index: 1;
-webkit-animation: slide-in-from-top 1s ease-in-out forwards;
-moz-animation: slide-in-from-top 1s ease-in-out forwards;
-o-animation: slide-in-from-top 1s ease-in-out forwards;
animation: slide-in-from-top 1s ease-in-out forwards;
}
.ny-2018-balls img {
cursor: pointer;
height: 100%;
position: absolute;
}
.ball {
transform-origin: center top;
}
.glow {
transform-origin: center center;
pointer-events: none;
-webkit-animation: glow 1.67s ease-in-out infinite;
-moz-animation: glow 1.67s ease-in-out infinite;
-o-animation: glow 1.67s ease-in-out infinite;
animation: glow 1.67s ease-in-out infinite;
}
#ball1 {
left: 0;
top: 9%;
height: 46%;
}
#ball2 {
left: 15%;
top: 3%;
height: 97%;
}
#ball3 {
left: 31%;
top: 0;
height: 71%;
}
#ball4 {
left: 51%;
top: 13%;
height: 87%;
}
#ball5 {
left: 72%;
top: 8%;
height: 61%;
}
#glow1 {
height: 14%;
left: -1.1%;
top: 42%;
transform-origin: center -231%;
opacity: 0;
animation-delay: 0.2s;
}
#glow2 {
height: 18.5%;
left: 13.5%;
top: 82.8%;
transform-origin: center -438%;
opacity: 0;
animation-delay: 0s;
}
#glow3 {
height: 23%;
left: 29.1%;
top: 49.6%;
transform-origin: center -213%;
opacity: 0;
animation-delay: 0.4s;
}
#glow4 {
height: 25.7%;
left: 49.3%;
top: 75.7%;
transform-origin: center -244%;
opacity: 0;
animation-delay: 1s;
}
#glow5 {
height: 28%;
left: 70%;
top: 43%;
transform-origin: center -124%;
opacity: 0;
animation-delay: 0.7s;
}
@-webkit-keyframes slide-in-from-top {
0% {
transform: translateY(-100%);
}
100% {
transform: translateY(0);
}
}
@-moz-keyframes slide-in-from-top {
0% {
transform: translateY(-100%);
}
100% {
transform: translateY(0);
}
}
@-o-keyframes slide-in-from-top {
0% {
transform: translateY(-100%);
}
100% {
transform: translateY(0);
}
}
@keyframes slide-in-from-top {
0% {
transform: translateY(-100%);
}
100% {
transform: translateY(0);
}
}
@-webkit-keyframes glow {
0% {
opacity: 0;
}
50% {
opacity: 1;
}
100% {
opacity: 0;
}
}
@-moz-keyframes glow {
0% {
opacity: 0;
}
50% {
opacity: 1;
}
100% {
opacity: 0;
}
}
@-o-keyframes glow {
0% {
opacity: 0;
}
50% {
opacity: 1;
}
100% {
opacity: 0;
}
}
@keyframes glow {
0% {
opacity: 0;
}
50% {
opacity: 1;
}
100% {
opacity: 0;
}
}
</style>
<script>
jQuery(
function( $ ) {
// Balls animation
function calcTransform( arrParams, percent ) {
var valS = 0;
var valR = 0;
var valO = 0;
var length = arrParams.length;
for ( var i = 0; i < length; i ++ ) {
var params = arrParams[i];
var time = ( params.init + percent ) * 100;
if ( time > 100 ) {
continue;
}
var cycles = 3;
var attenuation = 0.025;
var sin = Math.sin( time / 100 * 360 * Math.PI / 180 * cycles );
var amplitude = sin * Math.exp( - attenuation * time );
valS += amplitude * ( params.maxScale - 1 );
valR += amplitude * params.maxAngle;
var sin_90 = Math.sin( time / 100 * 360 * Math.PI / 180 * cycles - 90 * Math.PI / 180 );
valO = ( sin_90 + 1 ) / 2;
}
valO = valO / length;
valS = 1 + valS;
return {
transform: 'scale(' + valS + ') rotate(' + valR + 'deg)',
opacity: valO
};
}
$( '.ny-2018-balls .ball' ).hover(
function() {
if ( ! $( this ).hasClass( 'deaf' ) ) {
$( this ).addClass( 'deaf' );
var params = {
init: 0,
duration: 5000 + + ( Math.random() * 2 - 1 ) * 2000,
maxScale: 1 + ( Math.random() * 2 - 1 ) * 0.25,
maxAngle: 10 * ( Math.random() * 2 - 1 )
};
var arrParams = $( this ).data( 'arrParams' );
if ( typeof arrParams === 'undefined' ) {
arrParams = [];
} else {
var currentPercent = parseInt( $( this ).css( 'border-spacing' ) );
var length = arrParams.length;
for ( var i = 0; i < length - 1; i ++ ) {
arrParams[i].init += currentPercent;
if ( arrParams[i].init > 100 ) {
arrParams[i].init = 100;
}
}
arrParams[length - 1].init = currentPercent;
}
arrParams.push( params );
$( this ).data( 'arrParams', arrParams );
if ( ! $( this ).hasClass( 'animating' ) ) {
$( this ).addClass( 'animating' );
$( this ).dequeue().stop().animate(
{
// fake property, just to call step function
borderSpacing: 100 // must be 100, used as % of animation completed
},
{
step: function( now, fx ) {
var angle = now;
var percent = now / Math.abs( fx.end - fx.start );
var values = calcTransform( arrParams, percent );
$( this ).css( '-webkit-transform', values.transform );
$( this ).css( '-ms-transform', values.transform );
$( this ).css( 'transform', values.transform );
$( this ).next().css( '-webkit-transform', values.transform );
$( this ).next().css( '-ms-transform', values.transform );
$( this ).next().css( 'transform', values.transform );
},
duration: params.duration,
easing: 'linear',
complete: function() {
$( this ).removeClass( 'animating' ).dequeue();
$( this ).removeClass( 'deaf' );
$( this ).removeData( 'arrParams' );
$( this ).css( 'border-spacing', 0 );
}
}
);
}
}
}
);
}
);
</script>
<div class="ny-2018-wrapper">
<img src="https://kagg.eu/wp-content/uploads/2019/01/xmas-landscape.jpg" alt="XMas Landscape"
srcset="https://kagg.eu/wp-content/uploads/2019/01/xmas-landscape-960x541.jpg 960w, https://kagg.eu/wp-content/uploads/2019/01/xmas-landscape-200x113.jpg 200w, https://kagg.eu/wp-content/uploads/2019/01/xmas-landscape-540x304.jpg 540w, https://kagg.eu/wp-content/uploads/2019/01/xmas-landscape-768x433.jpg 768w, https://kagg.eu/wp-content/uploads/2019/01/xmas-landscape.jpg 1005w"
sizes="(max-width: 1005px) 100vw, 1005px">
<div class="ny-2018-balls">
<img class="ball" id="ball1" alt="ball1" src="https://kagg.eu/wp-content/uploads/2019/01/ball1.png">
<img class="glow" id="glow1" alt="glow1" src="https://kagg.eu/wp-content/uploads/2019/01/glow1.png">
<img class="ball" id="ball2" alt="ball2" src="https://kagg.eu/wp-content/uploads/2019/01/ball2.png">
<img class="glow" id="glow2" alt="glow2" src="https://kagg.eu/wp-content/uploads/2019/01/glow2.png">
<img class="ball" id="ball3" alt="ball3" src="https://kagg.eu/wp-content/uploads/2019/01/ball3.png">
<img class="glow" id="glow3" alt="glow3" src="https://kagg.eu/wp-content/uploads/2019/01/glow3.png">
<img class="ball" id="ball4" alt="ball4" src="https://kagg.eu/wp-content/uploads/2019/01/ball4.png">
<img class="glow" id="glow4" alt="glow4" src="https://kagg.eu/wp-content/uploads/2019/01/glow4.png">
<img class="ball" id="ball5" alt="ball5" src="https://kagg.eu/wp-content/uploads/2019/01/ball5.png">
<img class="glow" id="glow5" alt="glow5" src="https://kagg.eu/wp-content/uploads/2019/01/glow5.png">
</div>
</div>