Christmas balls like a damped pendulum

A realistic animation of Christmas balls which to be started to swing being touched by the mouse.

Christmas balls like a damped pendulum


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.

XMas Landscape
ball1 glow1 ball2 glow2 ball3 glow3 ball4 glow4 ball5 glow5


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>

Leave a Comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.